omnish 1.5.2 → 1.6.4
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 +8 -0
- package/README.md +6 -4
- package/config.example.json +4 -0
- package/dist/index.js +278 -268
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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
|
|
4
|
-
`,{mode:384})}function
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
var Ag=Object.defineProperty;var Ot=(e,t)=>()=>(e&&(t=e(e=0)),t);var Gc=(e,t)=>{for(var n in t)Ag(e,n,{get:t[n],enumerable:!0})};import Ig from"node:crypto";import oa from"node:fs";import Lg from"node:os";import ne from"node:path";function Og(){let e=process.env.OMNISH_HOME?.trim();if(e)return ne.resolve(e);let t=Lg.homedir(),n=ne.join(t,".omnish"),o=ne.join(t,".whatslive");try{if(oa.existsSync(n))return n;if(oa.existsSync(o))return o}catch{}return n}function Ko(e){let t=Ig.createHash("sha1").update(e,"utf8").digest("hex").slice(0,8);return ne.join(zc,t)}function U(e){oa.mkdirSync(e,{recursive:!0,mode:448})}function re(){U(D),U(ae),U(ut),U(zc),U(Jc),U(St),U(Nt),U(Xn),U(ia)}var D,ae,ut,zc,Jc,je,me,Vn,zo,vt,Wr,Dr,W,Ur,Hr,Br,St,ra,sa,qc,Nt,Jo,jr,Xn,dt,Zn,qo,ia,j=Ot(()=>{"use strict";D=Og(),ae=ne.join(D,"auth"),ut=ne.join(D,"jobs"),zc=ne.join(D,"apps"),Jc=ne.join(D,"logs"),je=ne.join(Jc,"gateway.log"),me=ne.join(D,"gateway.pid"),Vn=ne.join(D,"gateway-control.json"),zo=ne.join(D,"config-ui.json"),vt=ne.join(D,"ui.json"),Wr=ne.join(D,"tunnel-auth.json"),Dr=ne.join(D,"ui-server.json"),W=ne.join(D,"config.json"),Ur=ne.join(D,"shortcuts.json"),Hr=ne.join(D,"recipes.json"),Br=ne.join(D,"recipes-user.json"),St=ne.join(D,"cowork"),ra=ne.join(St,"tasks.json"),sa=ne.join(St,"pending-runs.json"),qc=ne.join(St,"completions.sqlite"),Nt=ne.join(D,"watch"),Jo=ne.join(Nt,"rules.json"),jr=ne.join(Nt,"events.sqlite"),Xn=ne.join(D,"bin"),dt=ne.join(D,"venvs","whisper"),Zn=ne.join(D,"node-transcribe"),qo=ne.join(D,"models","transformers"),ia=ne.join(D,"media","pull")});function Xc(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=Xc(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 Fg(e){let t=e.match(Yc);if(t)return t[1]??null;let n=e.match(Qc);if(n)return n[1]??null;let o=e.match(Vc);return o?o[1]??null:null}function Kc(e){let t=e.replace(/\D/g,"");return t?`+${t}`:""}function Vt(e){return`${e.replace(/\D/g,"")}@s.whatsapp.net`}function oe(e){let t=Xc(e);if(!t||Ng(t))return null;if(Yc.test(t)||Qc.test(t)||Vc.test(t)){let o=Fg(t);if(!o)return null;let r=Kc(o);return r.length>1?r:null}if(t.includes("@"))return null;let n=Kc(t);return n.length>1?n:null}function Gr(e){return e.map(t=>String(t).trim()).filter(t=>!!t).map(t=>t==="*"?t:oe(t)).filter(t=>!!t)}function zr(e){let t=new Set;for(let n of e){if(n==="*")continue;let o=oe(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 Jr(e){let t=new Set;for(let n of e){let o=Fe(String(n));o&&t.add(o)}return t}function aa(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=oe(t);return o?{kind:"wa",normalized:o}:null}function Zc(e,t){let n=oe(t);return n?e.has(n):!1}var Yc,Qc,Vc,ot=Ot(()=>{"use strict";Yc=/^(\d+)(?::\d+)?@s\.whatsapp\.net$/i,Qc=/^(\d+)@c\.us$/i,Vc=/^(\d+)@lid$/i});import qr from"node:fs";function _g(e){return typeof e.mediaSendFiles=="boolean"?e.mediaSendFiles:typeof e.pullAutoSend=="boolean"?e.pullAutoSend:E.mediaSendFiles}function Wg(e){return typeof e.mediaUrlAutoDl=="boolean"?e.mediaUrlAutoDl:typeof e.pullUrlAutoDetect=="boolean"?e.pullUrlAutoDetect:E.mediaUrlAutoDl}function Dg(e){return typeof e.mediaInstallFromChat=="boolean"?e.mediaInstallFromChat:typeof e.pullInstallFromChat=="boolean"?e.pullInstallFromChat:E.mediaInstallFromChat}function Ug(e){return typeof e.mediaOutputDir=="string"?e.mediaOutputDir.trim().slice(0,4096):typeof e.pullOutputDir=="string"?e.pullOutputDir.trim().slice(0,4096):E.mediaOutputDir}function Hg(e){let t=typeof e.mediaMaxBytes=="number"?e.mediaMaxBytes:typeof e.pullMaxBytes=="number"?e.pullMaxBytes:E.mediaMaxBytes;return!Number.isFinite(t)||t<0?E.mediaMaxBytes:t===0?0:Math.min(2e9,Math.floor(t))}function Bg(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:E.mediaWhisperModel;return String(t).trim().slice(0,64)}function jg(e){let t=typeof e.mediaWhisperDevice=="string"&&e.mediaWhisperDevice.trim().length>0?e.mediaWhisperDevice:E.mediaWhisperDevice,n=String(t).trim().toLowerCase().slice(0,16);return n==="cpu"||n==="cuda"||n==="auto"?n:E.mediaWhisperDevice}function Gg(e){let t=typeof e.mediaTranscribeTimeoutMs=="number"?e.mediaTranscribeTimeoutMs:E.mediaTranscribeTimeoutMs;return!Number.isFinite(t)||t<=0?E.mediaTranscribeTimeoutMs:Math.min(9e5,Math.max(6e4,Math.floor(t)))}function zg(e){let t=typeof e.mediaTranscribeEngine=="string"&&e.mediaTranscribeEngine.trim().length>0?e.mediaTranscribeEngine:E.mediaTranscribeEngine,n=String(t).trim().toLowerCase().slice(0,32);return n==="whisper"||n==="transformers"||n==="auto"?n:E.mediaTranscribeEngine}function Jg(e){return typeof e.mediaTranscribeFallback=="boolean"?e.mediaTranscribeFallback:E.mediaTranscribeFallback}function qg(e){return e==="downloads"||e==="omnishData"||e==="sessionCwd"||e==="processCwd"||e==="fixed"?e:E.fileReceiveRootMode}function Kg(e){return e==="primary"?"primary":"secondary"}function Yg(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=oe(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=oe(s);a&&(t[`wa:${a}`]=r.slice(0,64))}return t}function Yo(e){let t=typeof e.appsFlushMs=="number"&&e.appsFlushMs>=0?e.appsFlushMs:E.appsFlushMs,n=e.gatewayMode==="telegram"||e.gatewayMode==="both"||e.gatewayMode==="whatsapp"?e.gatewayMode:E.gatewayMode,o=typeof e.telegramBotToken=="string"?e.telegramBotToken:E.telegramBotToken,r=Array.isArray(e.telegramAllowFrom)?[...new Set(e.telegramAllowFrom.map(s=>Fe(String(s))).filter(s=>!!s))].sort():E.telegramAllowFrom;return{...E,...e,gatewayMode:n,telegramBotToken:o,telegramAllowFrom:r,allowFrom:Array.isArray(e.allowFrom)?Gr(e.allowFrom.map(String)).filter(s=>s!=="*"):E.allowFrom,commandPrefix:(()=>{let s=typeof e.commandPrefix=="string"&&e.commandPrefix.length>0?e.commandPrefix:E.commandPrefix;return s==="! "?"!":s})(),syncTimeoutMs:typeof e.syncTimeoutMs=="number"&&e.syncTimeoutMs>0?e.syncTimeoutMs:E.syncTimeoutMs,syncMaxBytes:typeof e.syncMaxBytes=="number"&&e.syncMaxBytes>0?e.syncMaxBytes:E.syncMaxBytes,jobLogTailLines:typeof e.jobLogTailLines=="number"&&e.jobLogTailLines>0?e.jobLogTailLines:E.jobLogTailLines,shell:typeof e.shell=="string"&&e.shell.length>0?e.shell:E.shell,appsCols:typeof e.appsCols=="number"&&e.appsCols>0&&e.appsCols<=500?Math.floor(e.appsCols):E.appsCols,appsRows:typeof e.appsRows=="number"&&e.appsRows>0&&e.appsRows<=200?Math.floor(e.appsRows):E.appsRows,appsFlushMs:t,appsMinIntervalMs:typeof e.appsMinIntervalMs=="number"&&e.appsMinIntervalMs>=0?e.appsMinIntervalMs:E.appsMinIntervalMs,appsMaxFlushBytes:typeof e.appsMaxFlushBytes=="number"&&e.appsMaxFlushBytes>256?Math.floor(e.appsMaxFlushBytes):E.appsMaxFlushBytes,appsMaxSessions:typeof e.appsMaxSessions=="number"&&e.appsMaxSessions>0?Math.min(50,Math.floor(e.appsMaxSessions)):E.appsMaxSessions,appsMaxSessionsTotal:typeof e.appsMaxSessionsTotal=="number"&&e.appsMaxSessionsTotal>0?Math.min(200,Math.floor(e.appsMaxSessionsTotal)):E.appsMaxSessionsTotal,appsMaxWaChars:typeof e.appsMaxWaChars=="number"&&e.appsMaxWaChars>256?Math.floor(e.appsMaxWaChars):E.appsMaxWaChars,appsLogTailLines:typeof e.appsLogTailLines=="number"&&e.appsLogTailLines>0?Math.min(500,Math.floor(e.appsLogTailLines)):E.appsLogTailLines,appsSubmitDelayMs:typeof e.appsSubmitDelayMs=="number"&&e.appsSubmitDelayMs>=0?Math.min(500,Math.floor(e.appsSubmitDelayMs)):E.appsSubmitDelayMs,appsClearInput:typeof e.appsClearInput=="boolean"?e.appsClearInput:E.appsClearInput,appsClearInputDelayMs:typeof e.appsClearInputDelayMs=="number"&&e.appsClearInputDelayMs>=0?Math.min(200,Math.floor(e.appsClearInputDelayMs)):E.appsClearInputDelayMs,appsClearInputSequence:typeof e.appsClearInputSequence=="string"&&e.appsClearInputSequence.length>0?e.appsClearInputSequence.slice(0,200):E.appsClearInputSequence,appsSkipClearOnPasswordPrompt:typeof e.appsSkipClearOnPasswordPrompt=="boolean"?e.appsSkipClearOnPasswordPrompt:E.appsSkipClearOnPasswordPrompt,appsPasswordPromptHint:typeof e.appsPasswordPromptHint=="boolean"?e.appsPasswordPromptHint:E.appsPasswordPromptHint,fileSendMaxBytes:typeof e.fileSendMaxBytes=="number"&&e.fileSendMaxBytes>=0?e.fileSendMaxBytes===0?0:Math.min(2e9,Math.floor(e.fileSendMaxBytes)):E.fileSendMaxBytes,fileReceiveMaxBytes:typeof e.fileReceiveMaxBytes=="number"&&e.fileReceiveMaxBytes>=0?e.fileReceiveMaxBytes===0?0:Math.min(2e9,Math.floor(e.fileReceiveMaxBytes)):E.fileReceiveMaxBytes,fileInboxSubdir:typeof e.fileInboxSubdir=="string"&&e.fileInboxSubdir.length>0&&e.fileInboxSubdir.replace(/[/\\]/g,"").slice(0,80)||E.fileInboxSubdir,fileReceiveRootMode:qg(e.fileReceiveRootMode),fileReceiveRootPath:typeof e.fileReceiveRootPath=="string"?e.fileReceiveRootPath.trim().slice(0,4096):E.fileReceiveRootPath,recipesAllowDangerousBuiltins:typeof e.recipesAllowDangerousBuiltins=="boolean"?e.recipesAllowDangerousBuiltins:E.recipesAllowDangerousBuiltins,recipesMaxTaskChars:typeof e.recipesMaxTaskChars=="number"&&e.recipesMaxTaskChars>=0?e.recipesMaxTaskChars===0?0:Math.min(Number.MAX_SAFE_INTEGER,Math.floor(e.recipesMaxTaskChars)):E.recipesMaxTaskChars,recipesMacroDefaultCommand:typeof e.recipesMacroDefaultCommand=="string"&&e.recipesMacroDefaultCommand.trim().length>0?e.recipesMacroDefaultCommand.trim().slice(0,4096):E.recipesMacroDefaultCommand,recipesRunAttach:typeof e.recipesRunAttach=="boolean"?e.recipesRunAttach:E.recipesRunAttach,clusterEnabled:typeof e.clusterEnabled=="boolean"?e.clusterEnabled:E.clusterEnabled,clusterLabel:typeof e.clusterLabel=="string"?e.clusterLabel.trim().slice(0,128):E.clusterLabel,clusterRole:Kg(e.clusterRole),clusterSenderBindings:Yg(e.clusterSenderBindings),serviceInstallFromChat:typeof e.serviceInstallFromChat=="boolean"?e.serviceInstallFromChat:E.serviceInstallFromChat,updateCheckEnabled:typeof e.updateCheckEnabled=="boolean"?e.updateCheckEnabled:E.updateCheckEnabled,updateCheckIntervalMs:typeof e.updateCheckIntervalMs=="number"&&e.updateCheckIntervalMs>0?Math.min(6048e5,Math.max(36e5,Math.floor(e.updateCheckIntervalMs))):E.updateCheckIntervalMs,updateCheckPackageName:typeof e.updateCheckPackageName=="string"&&e.updateCheckPackageName.trim().length>0?e.updateCheckPackageName.trim().slice(0,214):E.updateCheckPackageName,updateInfoUrl:typeof e.updateInfoUrl=="string"?e.updateInfoUrl.trim().slice(0,2048):E.updateInfoUrl,chatLlmFallbackEnabled:typeof e.chatLlmFallbackEnabled=="boolean"?e.chatLlmFallbackEnabled:E.chatLlmFallbackEnabled,chatLlmShellCommand:typeof e.chatLlmShellCommand=="string"?e.chatLlmShellCommand.trim().slice(0,8192):E.chatLlmShellCommand,chatLlmTimeoutMs:typeof e.chatLlmTimeoutMs=="number"&&e.chatLlmTimeoutMs>0?Math.min(9e5,Math.floor(e.chatLlmTimeoutMs)):E.chatLlmTimeoutMs,chatLlmMaxInputChars:typeof e.chatLlmMaxInputChars=="number"&&e.chatLlmMaxInputChars>0?Math.min(5e5,Math.floor(e.chatLlmMaxInputChars)):E.chatLlmMaxInputChars,chatLlmMaxOutputChars:typeof e.chatLlmMaxOutputChars=="number"&&e.chatLlmMaxOutputChars>0?Math.min(2e6,Math.floor(e.chatLlmMaxOutputChars)):E.chatLlmMaxOutputChars,chatLlmNeedsTty:typeof e.chatLlmNeedsTty=="boolean"?e.chatLlmNeedsTty:E.chatLlmNeedsTty,chatLlmWorkDir:typeof e.chatLlmWorkDir=="string"?e.chatLlmWorkDir.trim().slice(0,4096):E.chatLlmWorkDir,tunnelEnabled:typeof e.tunnelEnabled=="boolean"?e.tunnelEnabled:E.tunnelEnabled,tunnelRelayUrl:typeof e.tunnelRelayUrl=="string"&&e.tunnelRelayUrl.trim().length>0?e.tunnelRelayUrl.trim().slice(0,2048):E.tunnelRelayUrl,tunnelMaxActive:typeof e.tunnelMaxActive=="number"&&e.tunnelMaxActive>0?Math.min(50,Math.floor(e.tunnelMaxActive)):E.tunnelMaxActive,platformToken:typeof e.platformToken=="string"?e.platformToken.trim():E.platformToken,platformDeviceId:typeof e.platformDeviceId=="string"?e.platformDeviceId.trim().slice(0,128):E.platformDeviceId,webhookEnabled:typeof e.webhookEnabled=="boolean"?e.webhookEnabled:E.webhookEnabled,webhookPort:typeof e.webhookPort=="number"&&e.webhookPort>=0?Math.min(65535,Math.floor(e.webhookPort)):E.webhookPort,webhookHost:typeof e.webhookHost=="string"&&e.webhookHost.trim().length>0?e.webhookHost.trim():E.webhookHost,webhookToken:typeof e.webhookToken=="string"?e.webhookToken.trim():E.webhookToken,watchEnabled:typeof e.watchEnabled=="boolean"?e.watchEnabled:E.watchEnabled,watchDebounceMs:typeof e.watchDebounceMs=="number"&&Number.isFinite(e.watchDebounceMs)?Math.max(500,Math.min(6e4,Math.floor(e.watchDebounceMs))):E.watchDebounceMs,watchMaxEventsPerMinute:typeof e.watchMaxEventsPerMinute=="number"&&Number.isFinite(e.watchMaxEventsPerMinute)?Math.max(1,Math.min(120,Math.floor(e.watchMaxEventsPerMinute))):E.watchMaxEventsPerMinute,watchAutoRestore:typeof e.watchAutoRestore=="boolean"?e.watchAutoRestore:E.watchAutoRestore,mediaSendFiles:_g(e),mediaUrlAutoDl:Wg(e),mediaInstallFromChat:Dg(e),mediaOutputDir:Ug(e),mediaMaxBytes:Hg(e),mediaWhisperModel:Bg(e),mediaWhisperDevice:jg(e),mediaTranscribeTimeoutMs:Gg(e),mediaTranscribeEngine:zg(e),mediaTranscribeFallback:Jg(e),progressUpdates:typeof e.progressUpdates=="boolean"?e.progressUpdates:E.progressUpdates,pullYtDlpPath:typeof e.pullYtDlpPath=="string"?e.pullYtDlpPath.trim().slice(0,4096):E.pullYtDlpPath,pullFfmpegPath:typeof e.pullFfmpegPath=="string"?e.pullFfmpegPath.trim().slice(0,4096):E.pullFfmpegPath,pullWhisperPath:typeof e.pullWhisperPath=="string"?e.pullWhisperPath.trim().slice(0,4096):E.pullWhisperPath}}function A(e){let t=S(),n=Yo({...t,...e});return Ge(n),n}function S(){if(re(),!qr.existsSync(W)){let e=Yo({});return Ge(e),e}try{let e=qr.readFileSync(W,"utf8"),t=JSON.parse(e);return Yo(t)}catch{return Yo({})}}function Ge(e){re();let t=Yo(e);qr.writeFileSync(W,JSON.stringify(t,null,2)+`
|
|
3
|
+
`,{mode:384})}function Kr(e){let t=aa(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 Yr(e){let t=aa(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 Me(e){let t=typeof process.env.TELEGRAM_BOT_TOKEN=="string"?process.env.TELEGRAM_BOT_TOKEN.trim():"";return t||(e.telegramBotToken??"").trim()}function xt(e){let t=e.trim();return/^\d{6,}:[A-Za-z0-9_-]{20,}$/.test(t)}function Xt(e){let t=S();return t.telegramBotToken=e.trim(),Ge(t),S()}function eo(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 Qr(e){let t=S();return t.gatewayMode=e,Ge(t),S()}function Vr(e){let t=S();return t.clusterEnabled=e,Ge(t),S()}function pt(){try{return qr.readdirSync(ae).length>0}catch{return!1}}var E,ce=Ot(()=>{"use strict";j();ot();E={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",mediaWhisperDevice:"auto",mediaTranscribeTimeoutMs:6e5,mediaTranscribeEngine:"whisper",mediaTranscribeFallback:!0,progressUpdates:!0,pullYtDlpPath:"",pullFfmpegPath:"",pullWhisperPath:""}});import Qg from"pino";function to(){return process.env.OMNISH_VERBOSE==="1"||process.env.WHATSVERBOSE==="1"}function eu(){return to()?"info":"silent"}function la(e){e?process.env.OMNISH_VERBOSE="1":delete process.env.OMNISH_VERBOSE,M.level=eu()}function tu(){return M.child({module:"baileys"})}var M,xe=Ot(()=>{"use strict";M=Qg({level:eu(),base:{app:"omnish"}})});import Sa from"node:fs";function Dt(){try{let e=Sa.readFileSync(Wr,"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 Tt(e){re(),Sa.writeFileSync(Wr,JSON.stringify({token:e.token.trim(),...e.relayUrl?{relayUrl:e.relayUrl.trim()}:{}},null,2)+`
|
|
4
|
+
`,{mode:384})}function ps(){try{Sa.unlinkSync(Wr)}catch{}}function Ky(){return process.env.OMNISH_TOKEN?.trim()||process.env.OMNISH_TUNNEL_TOKEN?.trim()||process.env.OMNISH_DEVICE_TOKEN?.trim()||""}function $t(){let e=Ky();if(e)return e;let t=S().platformToken.trim();return t||(Dt()?.token??"")}function xa(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=Dt()?.relayUrl?.trim();return o?o.replace(/\/$/,""):e.replace(/\/$/,"")}function mt(e){return xa(e)}var Tn=Ot(()=>{"use strict";ce();j()});var Ee,$n=Ot(()=>{"use strict";Ee="https://tunnel.omnish.dev"});var sp={};Gc(sp,{chunksToSrt:()=>rp,isNodeTranscribeInstalled:()=>dn,mapWhisperModelToXenova:()=>op,transcribeWithTransformers:()=>Mw,transformersPackagePath:()=>An});import{spawnSync as xw}from"node:child_process";import un from"node:fs";import Pt from"node:path";import{pathToFileURL as Cw}from"node:url";function An(){return Pt.join(Zn,"node_modules","@xenova","transformers")}function dn(){try{let e=Pt.join(An(),"package.json");return un.existsSync(e)}catch{return!1}}function op(e){let t=e.trim().toLowerCase();return t in Va?Va[t]:t.startsWith("xenova/")?e.trim():Va.small}async function Rw(){if(!dn())throw new Error("Transformers.js not installed. Run: omnish pull install --transcribe-node");let e=JSON.parse(un.readFileSync(Pt.join(An(),"package.json"),"utf8")),t=e.exports?.["."],n=(typeof t=="string"?t:null)??e.main??"./dist/transformers.node.mjs",o=Pt.join(An(),n.replace(/^\.\//,""));return await import(Cw(o).href)}function np(e){let t=Math.max(0,Math.round(e*1e3)),n=Math.floor(t/36e5),o=Math.floor(t%36e5/6e4),r=Math.floor(t%6e4/1e3),s=t%1e3;return`${String(n).padStart(2,"0")}:${String(o).padStart(2,"0")}:${String(r).padStart(2,"0")},${String(s).padStart(3,"0")}`}function rp(e){let t=[],n=1;for(let o of e){let r=o.text?.trim(),s=o.timestamp;!r||!s||s.length<2||(t.push(String(n++)),t.push(`${np(s[0])} --> ${np(s[1])}`),t.push(r),t.push(""))}return t.join(`
|
|
5
|
+
`).trimEnd()}function Tw(e,t,n,o){let r=Pt.join(n,"_transformers_audio.wav");return xw(e,["-y","-i",t,"-vn","-ar","16000","-ac","1",r],{encoding:"utf8",timeout:o,windowsHide:!0}).status===0&&un.existsSync(r)?r:null}async function $w(e,t,n,o){let r=Pt.extname(t).toLowerCase();if([".wav",".mp3",".m4a",".flac",".ogg",".opus"].includes(r))return t;if(!e.ffmpeg)throw new Error("ffmpeg required to extract audio for Transformers.js (omnish pull install)");let s=Tw(e.ffmpeg,t,n,o);if(!s)throw new Error("ffmpeg audio extract failed");return s}async function Mw(e){let{cfg:t,tools:n,inputPath:o,outputDir:r,timeoutMs:s}=e;if(!un.existsSync(o))throw new Error(`File not found: ${o}`);let i=await Rw();i.env.cacheDir=qo,i.env.allowLocalModels=!0;let a=op(t.mediaWhisperModel.trim()||"small"),l=await $w(n,o,r,s),u=await(await i.pipeline("automatic-speech-recognition",a,{quantized:!0}))(l,{return_timestamps:!0,chunk_length_s:30,stride_length_s:5}),d=(u.text??"").trim(),p=u.chunks??[],f=Pt.basename(o,Pt.extname(o)),h=Pt.join(r,`${f}.txt`),g=Pt.join(r,`${f}.srt`);un.writeFileSync(h,d,"utf8"),p.length>0?un.writeFileSync(g,rp(p),"utf8"):d&&un.writeFileSync(g,`1
|
|
6
|
+
00:00:00,000 --> 00:00:30,000
|
|
7
|
+
${d}
|
|
8
|
+
`,"utf8");let y=[h,g].filter(b=>un.existsSync(b));if(y.length===0)throw new Error("Transformers.js produced no transcript files");return{files:y,log:`transformers.js (${a})`}}var Va,Ss=Ot(()=>{"use strict";j();Va={tiny:"Xenova/whisper-tiny",base:"Xenova/whisper-base",small:"Xenova/whisper-small",medium:"Xenova/whisper-medium",large:"Xenova/whisper-large-v2","large-v2":"Xenova/whisper-large-v2","large-v3":"Xenova/whisper-large-v2"}});function bn(e){return(e??"").trim()}function Xv(){return bn(process.env.OMNISH_PLATFORM_URL)||bn(process.env.OMNISH_COMM_LAYER_URL)||bn(process.env.OMNISH_TUNNEL_RELAY)}function Zv(){return bn(process.env.OMNISH_TOKEN)||bn(process.env.OMNISH_DEVICE_TOKEN)||bn(process.env.OMNISH_TUNNEL_TOKEN)}function Dl(){return $t()}function Lo(){return Zv()?"env":S().platformToken.trim()?"config":Dt()?.token?"file":"default"}function Sr(){let e=S();return xa(e.tunnelRelayUrl||Ee)}function Oo(){return Xv()?"env":S().tunnelRelayUrl.trim()?"config":Dt()?.relayUrl?.trim()?"file":"default"}function Ul(){let e=bn(process.env.OMNISH_DEVICE_ID);if(e)return e;let t=S().platformDeviceId.trim();if(t)return t}function Hl(){return bn(process.env.OMNISH_DEVICE_ID)?"env":S().platformDeviceId.trim()?"config":"default"}function he(){let e=Sr(),t=Dl();if(!e||!t)return null;let n=Ul();return{platformUrl:e.replace(/\/$/,""),token:t,deviceId:n}}var At=Ot(()=>{"use strict";ce();Tn();$n()});var ef={};Gc(ef,{fetchPlatformAccount:()=>jn,getAttachedConfig:()=>zl,getAttachedPlatformSnapshot:()=>nS,loadConfigForSendtoBroadcast:()=>Jl,mergeAttachedPlatformPolicy:()=>Gl,parsePlatformMeResponse:()=>Xm,setAttachedPlatformSnapshot:()=>Bl,setPlatformDefaultDevice:()=>ql,snapshotFromRegisteredAccount:()=>Zm,syncAttachedPlatformPolicy:()=>yi,updatePlatformAllowlists:()=>gi});function jl(e){let t=e.replace(/\/$/,"");return/^https?:\/\//i.test(t)?t:`http://${t}`}function eS(e){return e==="telegram"||e==="both"||e==="whatsapp"?e:"whatsapp"}function tS(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 Xm(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:eS(e.gatewayMode),connectors:tS(e.connectors),defaultDeviceId:r,routing:{defaultDeviceId:r,onlineDeviceIds:o,onlineCount:typeof n?.onlineCount=="number"?n.onlineCount:o.length}}}function Zm(e,t=[],n=[]){let o=e.defaultDeviceId??null;return{allowFrom:t,telegramAllowFrom:n,gatewayMode:e.gatewayMode,connectors:e.connectors,defaultDeviceId:o,routing:{defaultDeviceId:o,onlineDeviceIds:[],onlineCount:0}}}function Gl(e,t){return{...e,allowFrom:[...t.allowFrom],telegramAllowFrom:[...t.telegramAllowFrom],gatewayMode:t.gatewayMode}}function Bl(e){hi=e}function nS(){return hi}function zl(){let e=S();return hi?Gl(e,hi):e}async function Jl(){let e=S(),t=he();if(!t)return e;try{return Gl(e,await jn(t))}catch(n){return M.warn({err:String(n)},"sendto: could not fetch platform allowlists; using local config.json"),e}}async function jn(e){let t=await fetch(`${jl(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 Xm(n)}async function gi(e,t){let n=await fetch(`${jl(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 ql(e,t){let n=await fetch(`${jl(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 yi(e,t){try{let n=await jn(e);return Bl(n),M.info({gatewayMode:n.gatewayMode,waLinked:n.connectors.whatsapp?.linked===!0,tgLinked:n.connectors.telegram?.linked===!0,allowFromCount:n.allowFrom.length,telegramAllowFromCount:n.telegramAllowFrom.length},"attached platform policy loaded"),n}catch(n){if(t){let o=Zm(t);return Bl(o),M.warn({err:String(n)},"platform GET /v1/me failed; using register ack for gatewayMode/connectors only (allowlists from local config until /v1/me works)"),o}return M.warn({err:String(n)},"platform GET /v1/me failed; attached inbound uses local config.json allowlists"),null}}var hi,xr=Ot(()=>{"use strict";ce();xe();At();hi=null});ce();import LC from"node:dns";import OC from"node:crypto";import xn from"node:fs";import Bc from"node:path";import Cg from"node:os";ce();xe();import Fy from"node:os";import _y from"node:fs";j();import nu from"node:crypto";import Qo from"node:fs";import ca from"node:os";import no from"node:path";var Vg=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/;function da(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 et(e,t){let n=e.trim();return n===""||n==="."?no.resolve(t):n==="~"?ca.homedir():n.startsWith("~/")||n.startsWith("~\\")?no.join(ca.homedir(),n.slice(2)):no.isAbsolute(n)?n:no.resolve(t,n)}function Xr(e){return no.join(ca.homedir(),"Cowork",e)}function Xg(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:Xr(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,x=typeof k.kind=="string"?k.kind:"";if(x==="ondemand")d={kind:"ondemand"};else if(x==="daily"){let T=Number(k.hour),O=Number(k.minute);Number.isFinite(T)&&Number.isFinite(O)&&(d={kind:"daily",hour:T,minute:O})}else if(x==="weekdays"){let T=Number(k.hour),O=Number(k.minute);Number.isFinite(T)&&Number.isFinite(O)&&(d={kind:"weekdays",hour:T,minute:O})}else if(x==="hourly"){let T=Number(k.minute);Number.isFinite(T)&&Number.isInteger(T)&&T>=0&&T<=59&&(d={kind:"hourly",minute:T})}else if(x==="weekly"){let T=Number(k.hour),O=Number(k.minute),C=Number(k.weekday);Number.isFinite(T)&&Number.isFinite(O)&&Number.isInteger(C)&&(d={kind:"weekly",weekday:C,hour:T,minute:O})}else if(x==="heartbeat"){let T=Number(k.intervalMs),O=Number(k.graceMs);Number.isFinite(T)&&T>0&&Number.isFinite(O)&&O>0&&(d={kind:"heartbeat",intervalMs:T,graceMs:O})}}let p=null;typeof t.lastCompletedSlotMs=="number"&&Number.isFinite(t.lastCompletedSlotMs)&&(p=t.lastCompletedSlotMs);let f=typeof t.createdAtMs=="number"&&Number.isFinite(t.createdAtMs)?t.createdAtMs:Date.now(),h=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:h,attachFiles:g,lastCompletedSlotMs:p,createdAtMs:f}}function Pe(){try{let e=Qo.readFileSync(ra,"utf8"),t=JSON.parse(e);if(!t||!Array.isArray(t.tasks))return[];let n=[];for(let o of t.tasks){let r=Xg(o);r&&n.push(r)}return n}catch{return[]}}function De(e){U(St);let t={tasks:e},n=no.join(St,`.tasks.${process.pid}.${nu.randomBytes(4).toString("hex")}.tmp`);Qo.writeFileSync(n,JSON.stringify(t,null,2)+`
|
|
9
|
+
`,{mode:384}),Qo.renameSync(n,ra)}function rt(e,t,n){let o=t.trim().toLowerCase();return e.find(r=>r.name===o&&r.ownerPeerKey===n)}function Vo(){return nu.randomBytes(4).toString("hex")}function ou(e){U(St),Qo.writeFileSync(sa,JSON.stringify(e,null,2)+`
|
|
10
|
+
`,{mode:384})}function ru(e){let t=ua();t.push(e),ou(t)}function ua(){try{let e=Qo.readFileSync(sa,"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 su(e){if(e<=0)return{batch:[],remainingAfter:ua().length};let t=ua();if(t.length===0)return{batch:[],remainingAfter:0};let n=t.slice(0,e),o=t.slice(e);return ou(o),{batch:n,remainingAfter:o.length}}j();j();import Zg from"better-sqlite3";var iu=1,au=200,oo=null;function ey(){U(Nt);let e=new Zg(jr);e.pragma("journal_mode = WAL"),e.exec(`
|
|
7
11
|
CREATE TABLE IF NOT EXISTS watch_meta (
|
|
8
12
|
key TEXT PRIMARY KEY,
|
|
9
13
|
value TEXT NOT NULL
|
|
@@ -22,159 +26,164 @@ var Vf=Object.defineProperty;var Kt=(e,t)=>()=>(e&&(t=e(e=0)),t);var Xf=(e,t)=>{
|
|
|
22
26
|
state_key TEXT NOT NULL,
|
|
23
27
|
updated_at_ms INTEGER NOT NULL
|
|
24
28
|
);
|
|
25
|
-
`);let t=e.prepare("SELECT value FROM watch_meta WHERE key = 'schema_version'").get();return(t?Number(t.value):0)<
|
|
29
|
+
`);let t=e.prepare("SELECT value FROM watch_meta WHERE key = 'schema_version'").get();return(t?Number(t.value):0)<iu&&e.prepare("INSERT OR REPLACE INTO watch_meta (key, value) VALUES ('schema_version', ?)").run(String(iu)),e}function Xo(){return oo||(oo=ey()),oo}function Zr(){Xo()}function lu(){if(oo){try{oo.close()}catch{}oo=null}}function cu(e){let t=Xo();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>au&&t.prepare(`DELETE FROM watch_recent WHERE id IN (
|
|
26
30
|
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 Q(e){return{wa:
|
|
31
|
+
)`).run(n.c-au)}function uu(e,t){let n=Xo(),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 du(e){return Xo().prepare("SELECT state_key FROM watch_rule_state WHERE rule_id = ?").get(e)?.state_key??null}function pu(e,t,n){Xo().prepare("INSERT OR REPLACE INTO watch_rule_state (rule_id, state_key, updated_at_ms) VALUES (?, ?, ?)").run(e,t,n)}import yu from"node:path";import Ft from"node:path";function ty(e,t){let n=Ft.normalize(e);for(let o of t){let r=Ft.normalize(o);if(n===r||n.startsWith(r+Ft.sep))return!0}return!1}function ny(e,t,n){let o=Ft.relative(t,e);if(o.startsWith("..")||Ft.isAbsolute(o))return!1;let r=o.split(Ft.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 es(e,t){if(t.excludePaths.length&&ty(e,t.excludePaths))return!0;let n=t.path;if(n&&t.excludeGlobs.length){for(let o of t.excludeGlobs)if(ny(e,n,o))return!0}return!1}function mu(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=Ft.relative(e.path,n);!o.startsWith("..")&&!Ft.isAbsolute(o)&&t.push(o.split(Ft.sep).join("/")+"/**")}catch{}return t}ot();function ro(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=oe(String(r));s&&o.add(`wa:${Vt(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 fu(e,t,n){return ro(e,t,n)}xe();j();import oy from"node:crypto";import pa from"node:fs";var ry=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,sy=1,ma=20;function ts(e){let t=e.trim().toLowerCase();return ry.test(t)?{ok:!0,name:t}:{ok:!1,error:"Name must be alphanumeric with _ or -, max 32 chars."}}function ns(){return oy.randomBytes(8).toString("hex")}function iy(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 ay(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(h=>typeof h=="string"&&h.trim().length>0).map(h=>h.trim()):[],p=Array.isArray(t.excludePaths)?t.excludePaths.filter(h=>typeof h=="string"&&h.trim().length>0):[],f=Array.isArray(t.excludeGlobs)?t.excludeGlobs.filter(h=>typeof h=="string"&&h.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:iy(t.events),units:d,excludePaths:p,excludeGlobs:f,adapterStatus:typeof t.adapterStatus=="string"?t.adapterStatus:"",createdAtMs:typeof t.createdAtMs=="number"&&Number.isFinite(t.createdAtMs)?t.createdAtMs:Date.now()}}function ly(e){let t=JSON.parse(e);return!t||!Array.isArray(t.rules)?[]:t.rules.map(ay).filter(n=>n!==null)}function cy(e){let t=[...e].sort((s,i)=>s.createdAtMs-i.createdAtMs),n=new Set,o=!1,r=[];for(let s of t){if(!n.has(s.name)){n.add(s.name),r.push(s);continue}let i=s.id.slice(0,8),a=`${s.name}-${i}`;a.length>32&&(a=`${s.name.slice(0,32-i.length-1)}-${i}`);let l=0;for(;n.has(a);){l+=1;let c=String(l);a=`${s.name.slice(0,Math.max(1,32-i.length-c.length-1))}-${i}${c}`}n.add(a),M.warn({oldName:s.name,newName:a,id:s.id},"watch: renamed duplicate rule for device-wide namespace"),r.push({...s,name:a}),o=!0}return{rules:r,changed:o}}function Ue(){try{let e=pa.readFileSync(Jo,"utf8"),t=ly(e),{rules:n,changed:o}=cy(t);return o&&Ct(n),n}catch{return[]}}function uy(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 Zo(){let e=Ue();return{rules:e,summary:uy(e)}}function Ct(e){U(Nt);let n=`${JSON.stringify({version:sy,rules:e},null,2)}
|
|
32
|
+
`,o=`${Jo}.tmp`;pa.writeFileSync(o,n,{mode:384}),pa.renameSync(o,Jo)}function fa(){return Jo}function Cn(e,t){let n=t.trim().toLowerCase();return e.find(o=>o.name===n)}function hu(e,t){return e.find(n=>n.id===t)}function so(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 gu(e,t){let n=t.trim().toLowerCase();return e.filter(o=>o.name!==n)}import dy from"node:os";import Rt from"node:path";var py=new Set([".env","id_rsa","id_ed25519","id_ecdsa","known_hosts","credentials.json","secrets.json",".netrc","shadow","passwd"]);function st(e){let t=Rt.normalize(e),n=t.toLowerCase(),o=dy.homedir().toLowerCase();if(n.includes(`${Rt.sep}.ssh${Rt.sep}`)||n.endsWith(`${Rt.sep}.ssh`)||n.includes(`${Rt.sep}browser${Rt.sep}`)&&n.includes("profile")||o&&(n===`${o}/.gnupg`||n.startsWith(`${o}/.gnupg${Rt.sep}`))||n.includes(`${Rt.sep}keychains${Rt.sep}`))return!0;let r=Rt.basename(t);return!!(py.has(r)||r.startsWith(".env.")||r.endsWith(".pem")||r.endsWith(".key"))}var my=["node_modules",".git",".svn",".hg","__pycache__",".cache",".next","dist","build"],fy=[".tmp",".swp",".swx","~",".part"];function wu(e,t){let n=new Map,o=new Map;function r(u,d){let p=u.split(yu.sep);for(let h of p)if(my.includes(h))return!0;let f=yu.basename(u);for(let h of fy)if(f.endsWith(h))return!0;return!!es(u,d)}function s(u){let d=hu(Ue(),u);return!d||!d.enabled||d.paused?null:d}function i(u){let d=Date.now(),p=o.get(u);return!p||d-p.windowStart>=6e4?(o.set(u,{windowStart:d,count:1}),!1):p.count>=t.maxEventsPerMinute?!0:(p.count+=1,!1)}async function a(u,d){let p=e.getConfig();if(!p.watchEnabled)return;let f=s(u.id);if(!f||d.kind==="fs"&&d.meta?.path&&(st(d.meta.path)||r(d.meta.path,f)))return;if(cu(d),(f.notifyWhen??"always")==="state-change"){if(du(f.id)===d.stateKey)return;pu(f.id,d.stateKey,d.tsMs)}let g=fu(f.notify,f.ownerPeerKey,p);if(g.length===0)return;let y=`[watch:${f.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 p=s(d.ruleId);p&&(i(d.ruleId)||a(p,d.event))}function c(u){for(let[d,p]of n)p.ruleId===u&&(clearTimeout(p.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 p=`${u.id}:${d.stateKey||d.summary}`,f=n.get(p);f&&clearTimeout(f.timer);let h=setTimeout(()=>l(p),t.debounceMs);h.unref?.(),n.set(p,{timer:h,event:d,ruleId:u.id})},cancelForRule:c,dispose(){for(let u of n.values())clearTimeout(u.timer);n.clear(),o.clear()}}}function bu(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 ku from"node:fs";import hy from"node:path";import{subscribe as gy}from"@parcel/watcher";var yy=["**/node_modules/**","**/.git/**","**/.svn/**","**/.hg/**","**/__pycache__/**","**/.cache/**"];function wy(e){return e==="create"?"create":e==="delete"?"delete":e==="update"?"update":e}function by(e,t){try{let n=ku.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 vu(e,t){let n=e.path;if(!n||!ku.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 gy(n,(i,a)=>{if(i){r=`error: ${i.message}`,M.warn({rule:e.name,err:i.message},"watch fs adapter");return}for(let l of a){let c=wy(l.type);if(!bu(c,o))continue;let u=hy.resolve(l.path);if(!u.startsWith(n)||st(u)||es(u,e))continue;let d=by(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:[...yy,...mu(e)]})}catch(i){r=`error: ${String(i)}`,M.warn({rule:e.name,err:String(i)},"watch fs subscribe failed")}return{stop:async()=>{s&&(await s.unsubscribe().catch(()=>{}),s=null)},status:()=>r}}import $y from"node:os";xe();import Zt from"node:fs";function ky(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=Zt.statSync(e);if(u.size<r&&(r=0),u.size<=r)return;let d=Zt.openSync(e,"r");try{let p=u.size-r,f=Buffer.alloc(p);Zt.readSync(d,f,0,p,r),r=u.size;let h=f.toString("utf8");for(let g of h.split(`
|
|
33
|
+
`)){let y=g.trim();y&&t(y)}}finally{Zt.closeSync(d)}l="ok"}catch(u){l=`error: ${String(u)}`}}try{Zt.existsSync(e)&&(r=Zt.statSync(e).size),i=Zt.watch(e,()=>c()),i.on("error",()=>{i?.close(),i=null}),l="ok (watch)"}catch{l="ok (poll)",a=setInterval(c,o),a.unref?.(),c()}return{stop(){s=!0,i?.close(),a&&clearInterval(a)},status:()=>l}}function os(e,t){for(let n of e)try{if(Zt.existsSync(n))return ky(n,t)}catch(o){M.debug({path:n,err:String(o)},"watch log-tail skip path")}return null}var vy=["/var/log/install.log"];function Sy(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 Su(e,t){return os(vy,n=>{let o=Sy(n);o&&t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:o.slice(0,200),summary:o,tsMs:Date.now()})})}var xy=["/var/log/dpkg.log","/var/log/apt/history.log"];function Cy(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 Ry(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 xu(e,t){return os(xy,n=>{let o=Cy(n)??Ry(n);o&&t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:o,summary:o,tsMs:Date.now()})})}import{spawn as Ty}from"node:child_process";function Cu(e,t){let n=!1,o="ok",r=0,s=()=>{if(n)return;let a=Ty("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 p of d.sort((f,h)=>f.RecordId-h.RecordId)){if(p.RecordId<=r)continue;r=p.RecordId;let f=(p.Message??"").replace(/\s+/g," ").trim().slice(0,300);if(!f)continue;let h=`pkg: ${f}`;t({ruleId:e.id,ruleName:e.name,kind:"pkg",stateKey:`win:${p.RecordId}`,summary:h,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 Ru(e,t){let n=$y.platform();if(n==="win32"){let r=Cu(e,t);return{stop:()=>r.stop(),status:r.status}}if(n==="darwin"){let r=Su(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=xu(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 Ny from"node:os";import{spawnSync as Tu}from"node:child_process";var My=3e4;function Py(e){let t=new Map;for(let n of e){let o=Tu("launchctl",["print",`system/${n}`],{encoding:"utf8",timeout:1e4});if(o.status!==0){let s=process.getuid?.()??501,i=Tu("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 $u(e,t){let n=e.units,o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=Py(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,My).unref?.()};return i(),{stop(){o=!0},status:()=>r}}import{spawnSync as Mu}from"node:child_process";var Ey=3e4;function Ay(e){let t=new Map;if(e.length===0)return t;let n=["show",...e,"--property=ActiveState,SubState,UnitFileState","--no-pager"],o=Mu("systemctl",n,{encoding:"utf8",timeout:15e3});if(o.status!==0)return t;let r="";for(let s of(o.stdout??"").split(`
|
|
34
|
+
`)){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=Mu("systemctl",["is-active",i],{encoding:"utf8",timeout:5e3}),l=(a.stdout??a.stderr??"unknown").trim();t.set(i,l)}return t}function Pu(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=Ay(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,Ey).unref?.()};return i(),{stop(){o=!0},status:()=>r}}import{spawnSync as Iy}from"node:child_process";var Ly=3e4;function Oy(e){let t=new Map;for(let n of e){let o=Iy("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 Eu(e,t){let n=e.units,o=!1,r="ok",s=new Map,i=()=>{if(o)return;let a=Oy(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,Ly).unref?.()};return i(),{stop(){o=!0},status:()=>r}}function Au(e,t){let n=Ny.platform();return n==="win32"?Eu(e,t):n==="darwin"?$u(e,t):Pu(e,t)}var it=null,en=null;function Iu(e){let t=e.getConfig();return wu(e,{debounceMs:Math.max(500,t.watchDebounceMs??2e3),maxEventsPerMinute:Math.max(1,t.watchMaxEventsPerMinute??30)})}function Wy(e){return e.watchEnabled&&e.watchAutoRestore}var ha=class{deps;pipeline;runtimes=new Map;stopped=!1;constructor(t){this.deps=t,this.pipeline=Iu(t)}onEvent=(t,n)=>{this.pipeline.ingest(t,n)};cancelPendingForRule(t){this.pipeline.cancelForRule(t)}async reload(){if(this.pipeline.dispose(),this.pipeline=Iu(this.deps),await this.stopAdapters(),this.stopped||!this.deps.getConfig().watchEnabled)return 0;let n=Ue().filter(o=>o.enabled&&!o.paused);for(let o of n)await this.startRule(o);return this.runtimes.size}async startRule(t){try{let n;if(t.kind==="fs"){let o=et(t.path,Fy.homedir()),r={...t,path:o};if(st(o)){t.adapterStatus="denied: sensitive path",this.persistRuleStatus(t);return}if(!_y.existsSync(o)){t.adapterStatus="error: path not found",this.persistRuleStatus(t);return}let s=await vu(r,i=>this.onEvent(r,i));n={stop:()=>s.stop(),status:s.status}}else if(t.kind==="pkg")n=Ru(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=Au(t,r=>this.onEvent(t,r))}else return;t.adapterStatus=n.status(),this.persistRuleStatus(t),this.runtimes.set(t.id,{rule:t,adapter:n})}catch(n){t.adapterStatus=`error: ${String(n)}`,this.persistRuleStatus(t),M.warn({rule:t.name,err:String(n)},"watch rule start failed")}}persistRuleStatus(t){let n=Ue(),o=n.findIndex(r=>r.id===t.id);o>=0&&(n[o]={...n[o],adapterStatus:t.adapterStatus},Ct(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(),lu()}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 Lu(){return it}function ga(e){it?.cancelPendingForRule(e)}function Dy(){U(Nt),Zr()}function ya(){en&&(it||(it=new ha(en)),it.reload())}function rs(e){en=e,Dy();let t=e.getConfig(),{summary:n}=Zo();return n.total>0&&M.info({rules:n.total,active:n.active,paused:n.paused,disabled:n.disabled},"watch rules loaded from disk"),Wy(t)&&ya(),()=>{it?.stop(),it=null,en=null}}async function Ou(){return en?en.getConfig().watchEnabled?(ya(),it?.getRunningCount()??0):(it?.stop(),it=null,-1):-1}function Ye(){if(!en)return;if(!en.getConfig().watchEnabled){it?.stop(),it=null;return}ya()}j();ce();j();import By from"node:os";import as from"node:path";j();import wa from"node:fs";import Fu from"node:os";import tn from"node:path";var _u=tn.join(D,"sessions.json"),io=new Map;function Wu(){return{cwd:tn.resolve(Fu.homedir())}}function Uy(e){return e.startsWith("wa:")||e.startsWith("tg:")?e:`wa:${e}`}function Hy(){try{let e=wa.readFileSync(_u,"utf8"),t=JSON.parse(e);for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r=Uy(n),i={cwd:typeof o.cwd=="string"&&o.cwd.length>0?tn.resolve(o.cwd):Wu().cwd};o.fileReceiveRoot==="sessionCwd"&&(i.fileReceiveRoot="sessionCwd"),io.set(r,i)}}catch{}}function Du(){U(D);let e={};for(let[t,n]of io){let o={cwd:n.cwd};n.fileReceiveRoot==="sessionCwd"&&(o.fileReceiveRoot="sessionCwd"),e[t]=o}wa.writeFileSync(_u,JSON.stringify(e,null,2)+`
|
|
35
|
+
`,{mode:384})}var Nu=!1;function ba(){Nu||(Hy(),Nu=!0)}function se(e){ba();let t=io.get(e);return t||(t=Wu(),io.set(e,t)),t}function ss(e,t){ba();let n=tn.resolve(t),o=se(e);o.cwd=n,io.set(e,o),Du()}function is(e){return se(e).fileReceiveRoot==="sessionCwd"?"sessionCwd":"default"}function ka(e,t){ba();let n=se(e);t==="default"?delete n.fileReceiveRoot:n.fileReceiveRoot="sessionCwd",io.set(e,n),Du()}function Uu(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 Hu(e,t){if(t.kind==="home")return tn.resolve(Fu.homedir());let n=t.value.replace(/^['"]|['"]$/g,"");return tn.isAbsolute(n)?tn.normalize(n):tn.resolve(e,n)}function Bu(e){try{return wa.statSync(e).isDirectory()?{ok:!0}:{ok:!1,error:`Not a directory: ${e}`}}catch(t){return{ok:!1,error:String(t)}}}var jy="Omnish";function ls(){return as.join(By.homedir(),"Downloads",jy)}function nn(e,t){let n=se(t);if(n.fileReceiveRoot==="sessionCwd")return n.cwd;switch(e.fileReceiveRootMode){case"downloads":return ls();case"omnishData":return as.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(!as.isAbsolute(o))throw new Error('fileReceiveRootPath must be an absolute path when fileReceiveRootMode is "fixed".');return as.resolve(o)}default:return ls()}}j();import Ut from"node:fs";import Gu from"node:path";import an from"node:process";var Rn="\x1B";function Gy(e){return e.replace(/\u001B\[[\d;]*m/g,"")}function cs(e){return Gy(e).length}function va(e,t,n,o,r=2){let s=Math.max(0,t-cs(n));return`${e}${n}${" ".repeat(s)}${" ".repeat(r)}${o}`}function _t(e,t,n,o){if(n.length===0)return[];let r=n.map(i=>o(i.left)),s=Math.max(...r.map(cs));return n.map((i,a)=>va(t,s,r[a],v(e,i.right)))}var on={primary:"#b4ff24",foreground:"#eef2f4",muted:"#8a9199",border:"#2a3139",error:"#ef4444",warn:"#a0e614",warnAlt:"#8cce04"};function zy(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 rn(e){let t=zy(e);return t?`${Rn}[38;2;${t.r};${t.g};${t.b}m`:""}function us(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 Wt(e,t,n){return!t||!us(e)?n:`${t}${n}${Rn}[0m`}function q(e,t){return Wt(e,`${Rn}[1m`,t)}function Jy(e,t){return us(e)?`${rn(on.foreground)}${Rn}[1m${t}${Rn}[0m`:t}function X(e,t){return Wt(e,`${Rn}[2m`,t)}function Ce(e,t){return Wt(e,`${rn(on.primary)}${Rn}[1m`,t)}function fe(e,t){return Wt(e,rn(on.primary),t)}function v(e,t){return Wt(e,rn(on.foreground),t)}function w(e,t){return Wt(e,rn(on.muted),t)}function qy(e,t){return Wt(e,rn(on.border),t)}function ds(e,t){return Wt(e,rn(on.error),t)}function we(e,t){return Wt(e,rn(on.warn),t)}function sn(e,t=60,n="\u2500"){let o=n.repeat(Math.max(1,t));return qy(e,o)}function H(e,t){return us(e)?`${`${w(e,"[")}${fe(e,"omnish")}${w(e,"]")}`} ${t}`:`[omnish] ${t}`}function R(e,t){return us(e)?`${`${ds(e,"[omnish]")}`} ${t}`:`[omnish] ${t}`}function ju(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("",Jy(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(`
|
|
36
|
+
`).replace(/^\n+/,"").trimEnd()}ce();j();Tn();$n();function ms(e){return(e&4)!==0}function Ca(e){return(e&2)!==0}var zu={error:3,warn:2,info:1};function Ju(e,t){let n=zu[t];return e.filter(o=>zu[o.severity]>=n)}function Ht(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.`}),!Gu.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{Ut.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?Gu.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(an.platform!=="win32"){try{if(Ut.existsSync(W)){let c=Ut.statSync(W);(ms(c.mode)||Ca(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 an.getuid=="function"?an.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(Ut.existsSync(D)){let c=Ut.statSync(D);(ms(c.mode)||Ca(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&&Ut.existsSync(ut)){let c=Ut.statSync(ut);(ms(c.mode)||Ca(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(Ut.existsSync(ae)){let c=Ut.statSync(ae);ms(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 ${ae}`,fixHint:`chmod 700 ${ae}`})}}catch{}}return(typeof an.env.TELEGRAM_BOT_TOKEN=="string"?an.env.TELEGRAM_BOT_TOKEN.trim():"")&&n.push({severity:"info",code:"telegram-token-env",message:"TELEGRAM_BOT_TOKEN is set in the environment; it overrides config.json until unset.",fixHint:"Unset TELEGRAM_BOT_TOKEN in the shell/service env if you want config.json to apply."}),s&&!Me(e)&&n.push({severity:"error",code:"telegram-no-token",message:"Telegram is enabled for this gateway but no bot token is configured.",detail:"Set telegramBotToken in config or TELEGRAM_BOT_TOKEN.",fixHint:`Set telegramBotToken in ${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."}),(an.env.OMNISH_TOKEN?.trim()||an.env.OMNISH_TUNNEL_TOKEN?.trim()||an.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."}),$t()||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 er(e){return e.some(t=>t.severity==="error")}function Yy(e,t){switch(t){case"error":return ds(e,"[ERROR]");case"warn":return we(e,"[WARN]");case"info":return w(e,"[INFO]")}}function Ra(e,t,n,o){if(o.length!==0){t.push(q(e,`${n} (${o.length}):`));for(let r of o)t.push(` ${Yy(e,r.severity)} ${w(e,`${r.code}:`)} ${v(e,r.message)}`),r.detail&&t.push(` ${w(e,r.detail)}`),r.fixHint&&t.push(` ${w(e,`Fix: ${r.fixHint}`)}`);t.push("")}}function Ta(e,t){if(e.length===0)return[`${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(`
|
|
37
|
+
`);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 Ra(t,i,"Errors",n),Ra(t,i,"Warnings",o),Ra(t,i,"Notes",r),i.push(w(t,"Allowlisted identities can run commands as this user; treat them like passwords.")),i.join(`
|
|
38
|
+
`).trimEnd()}function $a(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 qu(e){return`${JSON.stringify({findings:e,summary:$a(e)},null,2)}
|
|
39
|
+
`}function Ku(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 Yu(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(ds(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 m(e){return{wa:e,tg:e}}function ye(e,t){return{wa:e,tg:t,tgHtml:!0}}function ue(e,t){return t==="whatsapp"?{text:e.wa}:e.tgHtml?{text:e.tg,parseModeHtml:!0}:{text:e.tg}}function Z(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function Re(e){return e.replace(/[*_~`]/g,t=>({"*":"\u2217",_:"\uFF3F","~":"\u02DC","`":"\u2032"})[t]??t)}function Qy(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(`
|
|
40
|
+
`).replace(/^\n+/,"").trimEnd()}function Vy(e){let t=[];for(let n of e)switch(n.kind){case"title":t.push("",`<b>${Z(n.text)}</b>`,"");break;case"sub":t.push("",`<i>${Z(n.text)}</i>`,"");break;case"gap":t.push("");break;case"p":t.push(Z(n.text));break;case"bullet":t.push(`\u2022 ${Z(n.text)}`);break}return t.join(`
|
|
41
|
+
`).replace(/^\n+/,"").trimEnd()}function Q(e){return{wa:Qy(e),tg:Vy(e),tgHtml:!0}}function ao(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 tr(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 Qu(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 Vu(){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 Ma(){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 Xu(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>${Z(e.gatewayMode)}</b> <i>current mode</i>`,"\u2022 whatsapp \u2014 WhatsApp only","\u2022 telegram \u2014 Telegram only","\u2022 both \u2014 both channels","",`${e.authPresent?"\u2713":"\u26A0"} WhatsApp auth: ${e.authPresent?"present":"missing \u2014 run omnish link"}`,`${e.tokenSet?"\u2713":"\u26A0"} Telegram token: ${e.tokenSet?"set":"not set"}`,`Allowlists: ${e.allowN} WA \xB7 ${e.tgAllowN} Telegram`,""];return e.updateBrief&&n.push(`<b>Updates</b> ${Z(e.updateBrief)}`,""),n.push("<i>Change:</i> /gateway both \xB7 /gw tg","<i>/updates</i> \u2014 npm + optional notice URL",Z(`Config: ${W}`)),ye(t.join(`
|
|
38
42
|
`),n.join(`
|
|
39
|
-
`))}function
|
|
43
|
+
`))}function nr(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>${Z(e.runningVersion)}</code>`,`<b>Checked (UTC)</b> ${Z(e.checkedAtIso)}`,`<b>npm package</b> <code>${Z(e.registryPackage)}</code>`];return e.registryError?n.push("",`<b>Registry</b> ${Z(e.registryError)}`):e.registryLatest&&n.push("",e.updateAvailable?`<b>Newer on npm</b> <code>${Z(e.registryLatest)}</code>`:`<b>npm latest</b> <code>${Z(e.registryLatest)}</code> (current or newer here)`),e.infoError?n.push("",`<b>Notice URL</b> ${Z(e.infoError)}`):e.infoMessage&&(n.push("",`<b>Notice</b> ${Z(e.infoMessage)}`),e.infoLink&&n.push(Z(e.infoLink))),n.push("","<i>Config keys:</i> updateCheckEnabled, updateCheckIntervalMs, updateCheckPackageName, updateInfoUrl"),ye(t.join(`
|
|
40
44
|
`),n.join(`
|
|
41
|
-
`))}function
|
|
45
|
+
`))}function Zu(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 ed(e){let t=!!Me(e);return[{kind:"title",text:"Telegram setup"},{kind:"p",text:"Configure on the host that runs omnish."},{kind:"gap"},{kind:"sub",text:"Steps"},{kind:"bullet",text:"@BotFather \u2192 /newbot \u2014 copy the API token."},{kind:"bullet",text:`Token: /tg token <paste>, or ${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 td(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>${Z(o.label)}</b> (${o.items.length})`,Z(r),"")}return ye(t.join(`
|
|
42
46
|
`).trimEnd(),n.join(`
|
|
43
|
-
`).trimEnd())}function
|
|
47
|
+
`).trimEnd())}function Pa(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>${Z(r.label)}</b> (${r.items.length})`,Z(s),"")}return ye(n.join(`
|
|
44
48
|
`).trimEnd(),o.join(`
|
|
45
|
-
`).trimEnd())}function
|
|
46
|
-
`),n=["<b>Token saved</b>","","telegramBotToken written to config (not echoed).",
|
|
49
|
+
`).trimEnd())}function nd(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(`
|
|
50
|
+
`),n=["<b>Token saved</b>","","telegramBotToken written to config (not echoed).",Z(`Config: ${W}`),...e.map(o=>`<i>${Z(o)}</i>`),"","Send /reload so the gateway picks it up."].join(`
|
|
47
51
|
`)+`
|
|
48
|
-
`;return
|
|
52
|
+
`;return ye(t.trimEnd(),n.trimEnd())}function od(){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 rd(e,t,n){let o=t?`
|
|
49
53
|
|
|
50
54
|
${t}`:n?`
|
|
51
55
|
|
|
52
56
|
Start omnish run on the host to apply (or /reload from a running gateway).`:"",r=t?`
|
|
53
57
|
|
|
54
|
-
${
|
|
58
|
+
${Z(t)}`:n?`
|
|
55
59
|
|
|
56
60
|
Start omnish run on the host to apply (or /reload from a running gateway).`:"",s=`*gatewayMode saved*
|
|
57
61
|
|
|
58
62
|
"${Re(e)}"
|
|
59
63
|
${Re(W)}${o}`,i=`<b>gatewayMode saved</b>
|
|
60
64
|
|
|
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
|
|
65
|
+
<code>${Z(e)}</code>
|
|
66
|
+
${Z(W)}${r}`;return ye(s,i)}function sd(){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 id(){return Q([{kind:"title",text:"/deny \u2014 remove from allowlist"},{kind:"p",text:"Same forms as /allow: +E164 or tg:id"}])}function ad(){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 Ea(){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 Aa(){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 Ia(){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 ld(e,t){let n=is(t),o=se(t),r="";try{r=nn(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 cd(){return Q([{kind:"title",text:"Unknown /wa command"},{kind:"p",text:"Send /wa help for setup."}])}function ud(){return Q([{kind:"title",text:"Unknown /tg command"},{kind:"p",text:"Send /tg help or /tg token <botfather_token>."}])}function dd(){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 pd(e){return Q([{kind:"title",text:"Unknown command"},{kind:"p",text:`Try /help or ${e.commandPrefix}<command>.`},{kind:"gap"},...ao(e)])}function Xy(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 md(e,t){let n=t?Xy(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"},...ao(e)),Q(o)}function fd(){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"},...Ma()];return Q(e)}function La(){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. Use $OMNISH_INPUT in the body for runtime text; for PTY agents and $OMNISH_TASK env 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:"bullet",text:"!name <input\u2026> or /name <input\u2026> \u2014 when the body contains $OMNISH_INPUT, text after the name replaces every $OMNISH_INPUT (required if the placeholder is present)"},{kind:"bullet",text:"Example: /shortcut add remosh /run remosh $OMNISH_INPUT \u2192 /remosh generate top 10 vid"},{kind:"p",text:"Shortcut bodies may start with !, /, etc.; nested shortcuts are not expanded."}]}function hd(e){if(e.length===0)return m("(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(`
|
|
67
|
+
`),a=["<b>Shortcuts</b>",r,"",...e.map(l=>{let u=`${s?l.scope==="chat"?"[chat] ":"[global] ":""}${l.name}`;return`\u2022 <code>${Z(u)}</code> \u2192 ${Z(l.body)}`})].join(`
|
|
68
|
+
`);return ye(i,a)}function or(e,t,n="chat"){return m(`Shortcut saved: ${e}
|
|
65
69
|
\u2192 ${t}
|
|
66
|
-
(${n==="global"?"Shared on this gateway (every chat unless this chat overrides the name).":"Stored for this chat only."})`)}function
|
|
70
|
+
(${n==="global"?"Shared on this gateway (every chat unless this chat overrides the name).":"Stored for this chat only."})`)}function gd(e,t="chat"){return m(`Shortcut removed (${t==="global"?"shared":"this chat"}): ${e}`)}function yd(e){return m(`Unknown shortcut: ${e}`)}function wd(e,t){return m(`Unknown shortcut "${e}" in ${t==="global"?"shared shortcuts":"this chat"}.`)}function Oa(e,t,n){let o=n?`
|
|
67
71
|
|
|
68
|
-
${n}`:"";return
|
|
69
|
-
\u2192 ${t}${o}`)}function
|
|
72
|
+
${n}`:"";return m(`${e}
|
|
73
|
+
\u2192 ${t}${o}`)}function bd(){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: "+Hr},{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 Zy(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 kd(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 ${Z(r.description)}`:"",c=s&&r.dangerous?Z(" [dangerous flags]"):"";n.push(`\u2022 ${Z(r.name)} \u2014 ${Z(r.label??r.name)}${l}${c}`)};if(e.featured.length>0){t.push("_Featured:_"),n.push("<i>Featured:</i>");for(let r of e.featured)o(r,!0);t.push(""),n.push("")}if(e.shared.length>0){t.push("_Shared (every chat):_"),n.push("<i>Shared (every chat):</i>");for(let r of e.shared)o(r,!1);t.push(""),n.push("")}if(e.yours.length>0){t.push("_This chat:_"),n.push("<i>This chat:</i>");for(let r of e.yours)o(r,!1);t.push(""),n.push("")}if(e.more.length>0){t.push("_More:_"),n.push("<i>More:</i>");for(let r of e.more)o(r,!1)}return e.featured.length===0&&e.shared.length===0&&e.yours.length===0&&e.more.length===0&&(t.push("(no recipes \u2014 add host file or /run add)"),n.push(Z("(no recipes \u2014 add host file or /run add)"))),ye(t.join(`
|
|
70
74
|
`).trimEnd(),n.join(`
|
|
71
|
-
`).trimEnd())}function
|
|
72
|
-
`))}function
|
|
73
|
-
`))}function
|
|
75
|
+
`).trimEnd())}function Na(e,t){let n=e.taskEnv??"OMNISH_TASK",o=[`Recipe: ${e.name}`,`Source: ${Zy(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),m(o.join(`
|
|
76
|
+
`))}function rr(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})`),m(r.join(`
|
|
77
|
+
`))}function vd(e,t="chat"){return m(`Recipe removed (${t==="global"?"gateway-shared storage":"this chat"}): ${e}`)}function Sd(e,t){return m(`Unknown recipe "${e}" in ${t==="global"?"gateway-shared storage":"this chat storage"}.`)}function xd(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 m(t==="global"?"(no gateway-shared recipes \u2014 /run add --global <name> <command\u2026>)":"(no recipes in this chat \u2014 /run add <name> <command\u2026>)");let r=["*Recipes*",n,""],s=["<b>Recipes</b>",o,""];for(let i of e){let a=i.description?` \u2014 ${i.description}`:"";r.push(`${Ae}${i.name} \u2014 ${i.label??i.name}${a}`);let l=i.description?` \u2014 ${Z(i.description)}`:"";s.push(`\u2022 ${Z(i.name)} \u2014 ${Z(i.label??i.name)}${l}`)}return ye(r.join(`
|
|
74
78
|
`).trimEnd(),s.join(`
|
|
75
|
-
`).trimEnd())}function
|
|
76
|
-
/run list`)}function
|
|
77
|
-
`)}function
|
|
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
|
|
79
|
+
`).trimEnd())}function Fa(e){return m(`Unknown recipe: ${e}
|
|
80
|
+
/run list`)}function _a(){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 ew(e){let{errors:t,warns:n,infos:o}=$a(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 Cd(e){return Q(ew(e))}function Rd(){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 Td(){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 lo(e){let t=e.trim();return t?t.length<=8?"(set)":`${t.slice(0,4)}\u2026${t.slice(-4)}`:"(empty)"}function $d(e){let t=e.trim();return(t.startsWith('"')&&t.endsWith('"')&&t.length>=2||t.startsWith("'")&&t.endsWith("'")&&t.length>=2)&&(t=t.slice(1,-1)),t}function Ie(e){let t=e.trim().toLowerCase();return t==="true"||t==="1"||t==="yes"||t==="on"?!0:t==="false"||t==="0"||t==="no"||t==="off"?!1:null}var Md=new Set(["downloads","omnishData","sessionCwd","processCwd","fixed"]),ln=["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","mediaWhisperDevice","mediaTranscribeTimeoutMs","mediaTranscribeEngine","mediaTranscribeFallback","progressUpdates","pullYtDlpPath","pullFfmpegPath","pullWhisperPath"];function Pd(e){return ln.includes(e)}function tw(e){let t=Me(e);return["*Config* (secrets masked)","",`gatewayMode: ${e.gatewayMode}`,`commandPrefix: ${e.commandPrefix}`,`shell: ${e.shell}`,`syncTimeoutMs: ${e.syncTimeoutMs}`,`syncMaxBytes: ${e.syncMaxBytes}`,`jobLogTailLines: ${e.jobLogTailLines}`,"","*Cluster*",`clusterEnabled: ${e.clusterEnabled}`,`clusterRole: ${e.clusterRole}`,`clusterLabel: ${e.clusterLabel||"(hostname)"}`,`clusterSenderBindings: ${Object.keys(e.clusterSenderBindings??{}).length} entries`,"","*Telegram*",`telegramBotToken: ${lo(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: ${lo(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}`,`mediaWhisperDevice: ${e.mediaWhisperDevice}`,`mediaTranscribeTimeoutMs: ${e.mediaTranscribeTimeoutMs}`,`mediaTranscribeEngine: ${e.mediaTranscribeEngine}`,`mediaTranscribeFallback: ${e.mediaTranscribeFallback}`,`progressUpdates: ${e.progressUpdates}`,"",`File: ${W}`].join(`
|
|
81
|
+
`)}function nw(e){let t=Me(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(lo(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(lo(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(`
|
|
82
|
+
`)}function Ed(e,t){if(!Pd(t))return null;let n=t;if(n==="telegramBotToken")return`telegramBotToken: ${lo(Me(e))}`;if(n==="webhookToken")return`webhookToken: ${lo(e.webhookToken)}`;let o=e[n];return`${t}: ${typeof o=="object"?JSON.stringify(o):String(o)}`}function ow(e,t){let n=Ed(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 fs(e,t){let n=$d(t),o=!1,r=!1,s=!1;if(e==="telegramBotToken"){if(!xt(n))throw new Error("Invalid bot token format (expect digits:secret from BotFather).");return Xt(n),o=!0,s=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s}}let i=S();switch(e){case"gatewayMode":{let a=eo(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 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=$d(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(!Md.has(a))throw new Error(`fileReceiveRootMode: ${[...Md].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 A({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 A({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 A({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 A({chatLlmFallbackEnabled:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmShellCommand":return A({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 A({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 A({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 A({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 A({chatLlmNeedsTty:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"chatLlmWorkDir":return A({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 A({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 A({webhookPort:a}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"webhookHost":if(!n.trim())throw new Error("webhookHost: non-empty bind address");return A({webhookHost:n.trim().slice(0,256)}),o=!0,{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"webhookToken":return A({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 A({watchEnabled:a}),Ye(),{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 A({watchDebounceMs:a}),Ye(),{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 A({watchMaxEventsPerMinute:a}),Ye(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"watchAutoRestore":{let a=Ie(n);if(a===null)throw new Error("watchAutoRestore: true or false");return A({watchAutoRestore:a}),Ye(),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaSendFiles":{let a=Ie(n);if(a===null)throw new Error("mediaSendFiles: true or false");return A({mediaSendFiles:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaInstallFromChat":{let a=Ie(n);if(a===null)throw new Error("mediaInstallFromChat: true or false");return A({mediaInstallFromChat:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaUrlAutoDl":{let a=Ie(n);if(a===null)throw new Error("mediaUrlAutoDl: true or false");return A({mediaUrlAutoDl:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaOutputDir":return A({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 A({mediaMaxBytes:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaWhisperModel":return A({mediaWhisperModel:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"mediaWhisperDevice":{let a=n.trim().toLowerCase();if(a!=="auto"&&a!=="cpu"&&a!=="cuda")throw new Error("mediaWhisperDevice: auto, cpu, or cuda");return A({mediaWhisperDevice:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaTranscribeTimeoutMs":{let a=Number.parseInt(n.trim(),10);if(!Number.isFinite(a)||a<=0)throw new Error("mediaTranscribeTimeoutMs: positive integer (ms)");return A({mediaTranscribeTimeoutMs:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaTranscribeEngine":{let a=n.trim().toLowerCase();if(a!=="whisper"&&a!=="transformers"&&a!=="auto")throw new Error("mediaTranscribeEngine: whisper, transformers, or auto");return A({mediaTranscribeEngine:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"mediaTranscribeFallback":{let a=Ie(n);if(a===null)throw new Error("mediaTranscribeFallback: true or false");return A({mediaTranscribeFallback:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"progressUpdates":{let a=Ie(n);if(a===null)throw new Error("progressUpdates: true or false");return A({progressUpdates:a}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}case"pullYtDlpPath":return A({pullYtDlpPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"pullFfmpegPath":return A({pullFfmpegPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s};case"pullWhisperPath":return A({pullWhisperPath:n.trim()}),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}return Ge(i),{reloadSuggested:o,shellWarning:r,tokenSaved:s}}async function Ad(e,t){let n=e.trim(),o=n.toLowerCase();if(!n||o==="help")return Q(Vu());if(o==="keys"||o==="help keys")return m(["*Configurable keys* (/config set)","",ln.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(`
|
|
83
|
+
`));if(o==="show"){let i=S();return ye(tw(i),nw(i))}let r=n.match(/^get\s+(\S+)\s*$/i);if(r){let i=r[1],a=S(),l=Ed(a,i);if(!l)return m(`Unknown key "${i}". /config keys`);let c=ow(a,i);return ye(`${l}
|
|
80
84
|
|
|
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(!
|
|
85
|
+
${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(!Pd(i))return m(`Unknown key "${i}". /config keys`);try{let{reloadSuggested:l,shellWarning:c,tokenSaved:u}=fs(i,a),d="";if(t?.reload&&l){let y=await t.reload();d=y.ok?`
|
|
82
86
|
Reload: ${y.summary}`:`
|
|
83
87
|
Reload failed: ${y.error}`}else l?d=`
|
|
84
88
|
Send /reload while omnish run is active (gatewayMode / Telegram).`:d=`
|
|
85
|
-
Send /reload to pick up changes where applicable.`;let
|
|
86
|
-
\u26A0 shell changed \u2014 affects all commands run via omnish.`:"",
|
|
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
|
-
|
|
89
|
-
${n}`}function
|
|
90
|
-
`,{mode:384}),t}function
|
|
91
|
-
`,r=
|
|
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(", "),
|
|
89
|
+
Send /reload to pick up changes where applicable.`;let p=c?`
|
|
90
|
+
\u26A0 shell changed \u2014 affects all commands run via omnish.`:"",f=u?`
|
|
91
|
+
telegramBotToken saved (not echoed).`:"",h=`Set *${i}* (saved).${f}${p}${d}`,g=`<b>Set ${Te(i)}</b> (saved).${f?"<br/>token saved (not echoed).":""}${c?"<br/>\u26A0 shell path changed.":""}${Te(d)}`;return ye(h,g)}catch(l){return m(`Error: ${String(l)}`)}}return m("Unknown /config command. Try /config help or /config show")}ce();var Wa=[...ln,"platformToken","platformDeviceId"],Da={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 Wa)Da[e]=e;function hs(e){let t=e.trim().toLowerCase().replace(/-/g,"_");return Da[t]??null}function Id(){return Object.keys(Da).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 rw(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 sw(e){return E[e]}function co(e,t){let n=rw(t),o=S();switch(e){case"platformToken":return A({platformToken:n});case"platformDeviceId":return A({platformDeviceId:n});case"gatewayMode":{let r=eo(n);if(!r)throw new Error('gatewayMode: "whatsapp", "telegram", or "both"');return o.gatewayMode=r,Ge(o),o}case"telegramBotToken":if(!xt(n))throw new Error("telegramBotToken: invalid bot token format");return Xt(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 A({tunnelRelayUrl:r})}case"tunnelEnabled":{let r=Le(n);if(r===null)throw new Error("tunnelEnabled: true or false");return A({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 A({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 A({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 A({chatLlmFallbackEnabled:r})}else{if(e==="chatLlmShellCommand")return A({chatLlmShellCommand:n});if(e==="chatLlmTimeoutMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("chatLlmTimeoutMs: positive integer");return A({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 A({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 A({chatLlmMaxOutputChars:r})}else if(e==="chatLlmNeedsTty"){let r=Le(n);if(r===null)throw new Error("chatLlmNeedsTty: true or false");return A({chatLlmNeedsTty:r})}else{if(e==="chatLlmWorkDir")return A({chatLlmWorkDir:n.trim()});if(e==="webhookEnabled"){let r=Le(n);if(r===null)throw new Error("webhookEnabled: true or false");return A({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 A({webhookPort:r})}else if(e==="webhookHost"){if(!n.trim())throw new Error("webhookHost: non-empty");return A({webhookHost:n.trim().slice(0,256)})}else{if(e==="webhookToken")return A({webhookToken:n.trim()});if(e==="watchEnabled"){let r=Le(n);if(r===null)throw new Error("watchEnabled: true or false");return A({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 A({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 A({watchMaxEventsPerMinute:r})}else if(e==="watchAutoRestore"){let r=Le(n);if(r===null)throw new Error("watchAutoRestore: true or false");return A({watchAutoRestore:r})}else if(e==="mediaSendFiles"){let r=Le(n);if(r===null)throw new Error("mediaSendFiles: true or false");return A({mediaSendFiles:r})}else if(e==="mediaInstallFromChat"){let r=Le(n);if(r===null)throw new Error("mediaInstallFromChat: true or false");return A({mediaInstallFromChat:r})}else if(e==="mediaUrlAutoDl"){let r=Le(n);if(r===null)throw new Error("mediaUrlAutoDl: true or false");return A({mediaUrlAutoDl:r})}else{if(e==="mediaOutputDir")return A({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 A({mediaMaxBytes:r})}else{if(e==="mediaWhisperModel")return A({mediaWhisperModel:n.trim()});if(e==="mediaWhisperDevice"){let r=n.trim().toLowerCase();if(r!=="auto"&&r!=="cpu"&&r!=="cuda")throw new Error("mediaWhisperDevice: auto, cpu, or cuda");return A({mediaWhisperDevice:r})}else if(e==="mediaTranscribeTimeoutMs"){let r=Number.parseInt(n,10);if(!Number.isFinite(r)||r<=0)throw new Error("mediaTranscribeTimeoutMs: positive integer");return A({mediaTranscribeTimeoutMs:r})}else if(e==="mediaTranscribeEngine"){let r=n.trim().toLowerCase();if(r!=="whisper"&&r!=="transformers"&&r!=="auto")throw new Error("mediaTranscribeEngine: whisper, transformers, or auto");return A({mediaTranscribeEngine:r})}else if(e==="mediaTranscribeFallback"){let r=Le(n);if(r===null)throw new Error("mediaTranscribeFallback: true or false");return A({mediaTranscribeFallback:r})}else if(e==="progressUpdates"){let r=Le(n);if(r===null)throw new Error("progressUpdates: true or false");return A({progressUpdates:r})}else{if(e==="pullYtDlpPath")return A({pullYtDlpPath:n.trim()});if(e==="pullFfmpegPath")return A({pullFfmpegPath:n.trim()});if(e==="pullWhisperPath")return A({pullWhisperPath:n.trim()});throw new Error(`Unsupported key: ${e}`)}}}}}}return Ge(o),o}function Ld(e){let t=sw(e);if(e==="platformToken"||e==="platformDeviceId")return A({[e]:t});if(e==="tunnelRelayUrl")return A({tunnelRelayUrl:t});let n=S();return n[e]=t,Ge(n),n}function Mn(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}ce();j();import uo from"node:fs";import ja from"node:os";import sr from"node:path";import _d from"node:crypto";var Ha="\u2063omnish/c v1",iw=/([nlra])=([^\s\]]*)/g;function ft(e){return e.replace(/-/g,"").slice(0,8)}function Ua(e){return e.replace(/[\s\]\r\n]/g,"_").slice(0,64)}function aw(e){let t=Ua(ft(e.nodeId)),n=Ua(e.label||""),o=e.role==="primary"?"p":"s",r=Ua(e.activeNodeId?ft(e.activeNodeId):"");return`${Ha} [n=${t} l=${n} r=${o} a=${r}]`}function Od(e,t){let n=aw(t);if(e.includes(n))return e;let o=e.replace(/\s+$/,"");return o.length===0?n:`${o}
|
|
92
|
+
|
|
93
|
+
${n}`}function Nd(e){if(!e||!e.includes(Ha))return null;let t=e.indexOf(Ha),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(iw))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,lw="node-id",cw="cluster-local.json",Fd=8;function Dd(){return sr.join(D,cw)}function Ga(){return sr.join(D,lw)}function at(){U(D);let e=Ga();try{if(uo.existsSync(e)){let n=uo.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=_d.randomUUID();return uo.writeFileSync(e,`${t}
|
|
94
|
+
`,{mode:384}),t}function Ud(){return{schemaVersion:Wd,updatedAt:new Date().toISOString(),peers:[],senderBindings:{}}}function uw(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=Dd();try{let t=uo.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=uw(n.senderBindings);return{schemaVersion:Wd,updatedAt:typeof n.updatedAt=="string"?n.updatedAt:new Date().toISOString(),peers:o,senderBindings:r}}catch{return Ud()}}function dw(e,t){let n=sr.dirname(e);U(n);let o=`${JSON.stringify(t,null,2)}
|
|
95
|
+
`,r=sr.join(n,`.${sr.basename(e)}.tmp.${process.pid}.${_d.randomBytes(4).toString("hex")}`);uo.writeFileSync(r,o,{mode:384}),uo.renameSync(r,e)}function Ja(e){let t=Dd(),n=Ud();for(let o=0;o<Fd;o++){n=ke(),e(n),n.updatedAt=new Date().toISOString();try{return dw(t,n),n}catch{}}throw new Error(`Could not write cluster local state after ${Fd} attempts: ${t}`)}function Hd(e){return(e.clusterLabel??"").trim()||ja.hostname()}function ve(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function Bd(e,t){let n=e.peers.findIndex(o=>o.nodeId===t.nodeId);n>=0?e.peers[n]=t:e.peers.push(t)}function ir(e){return{nodeId:ft(at()),label:Hd(e),role:e.clusterRole,lastSeenIso:new Date().toISOString()}}function jd(e,t,n){let o=n.trim();if(!o)return{ok:!1,reason:"not-found"};let r=ir(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 Bt(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=jd(n,e,r);if(s.ok)return{senderKey:t,nodeId:s.peer.nodeId,sinceIso:new Date(0).toISOString(),source:"config"}}return null}function za(e,t,n="chat"){let o=S(),r=ke(),s=jd(r,o,t);if(!s.ok)return{state:r,resolved:s};let i=new Date().toISOString();return{state:Ja(l=>{l.senderBindings[e]={senderKey:e,nodeId:s.peer.nodeId,sinceIso:i,source:n},Bd(l,{...s.peer,lastSeenIso:i})}),resolved:s}}function pw(e){let t=null;return{state:Ja(o=>{o.senderBindings[e]&&(t=o.senderBindings[e]??null,delete o.senderBindings[e])}),removed:t}}function Gd(e,t){let n=ft(at()),o=new Date().toISOString();return Ja(r=>{if(Bd(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 zd(e,t){let n=Bt(t,e);return n?n.nodeId===ft(at()):!1}function mw(e,t){let n=ir(t).nodeId,o=new Set([n]);for(let s of e.peers)o.add(s.nodeId);return[...o].sort()[0]??n}function Qe(e,t){return mw(e,t)===ft(at())}function gs(e,t,n){let o=ft(at()),r=["*Computers*",""],s=n?Bt(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,ir(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(", "),p=d?` (${d})`:"";r.push(`${Ae}*${l.label}*${p}
|
|
93
97
|
id \`${l.nodeId}\` \xB7 role ${l.role}
|
|
94
98
|
seen ${l.lastSeenIso}`)}return r.push(""),r.push(`Updated: ${e.updatedAt}`),r.join(`
|
|
95
|
-
`)}function
|
|
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(", "),
|
|
97
|
-
`)}function
|
|
98
|
-
`),d=["<b>This computer</b>","",`label: ${ve(i)}`,`node id: <code>${ve(o)}</code> (full id in ${ve(
|
|
99
|
-
`);return{wa:u,tg:d}}function
|
|
99
|
+
`)}function Ba(e,t,n){let o=ft(at()),r=["<b>Computers</b>",""],s=n?Bt(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,ir(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(`
|
|
100
|
+
`);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(", "),p=d?` (${ve(d)})`:"";r.push(`\u2022 <b>${ve(l.label)}</b>${p}<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(`
|
|
101
|
+
`)}function qa(e,t,n){return gs(e,t,n??null).replace(/\*([^*]+)\*/g,"$1").replace(/`([^`]+)`/g,"$1")}function Ka(e,t){let n=at(),o=ft(n),r=ke(),s=e.clusterRole,i=Hd(e),a=t?Bt(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 ${Ga()})`,`host: ${ja.hostname()}`,`clusterRole: ${s}`,c,`peers known: ${r.peers.length}`,`senderBindings (chat): ${Object.keys(r.senderBindings).length}`].filter(Boolean).join(`
|
|
102
|
+
`),d=["<b>This computer</b>","",`label: ${ve(i)}`,`node id: <code>${ve(o)}</code> (full id in ${ve(Ga())})`,`host: ${ve(ja.hostname())}`,`clusterRole: ${ve(s)}`,t?ve(c):"",`peers known: ${r.peers.length}`,`senderBindings (chat): ${Object.keys(r.senderBindings).length}`].filter(Boolean).join(`
|
|
103
|
+
`);return{wa:u,tg:d}}function fw(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
104
|
`),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
|
|
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.","",
|
|
103
|
-
`);return
|
|
104
|
-
`),u=["<b>Bound to this machine.</b>","","Your messages now route here. Other senders are not affected.","",
|
|
105
|
-
`);return
|
|
106
|
-
`),
|
|
107
|
-
`);return
|
|
105
|
+
`);return ye(n,o)}function hw(e,t){if(t.ok)return m("");if(t.reason==="ambiguous-label"){let n=(t.matches??[]).map(o=>`\`${o.nodeId}\` (${o.label})`).join(", ");return m(`Label "${e}" matches multiple machines: ${n}. Use the 8-character id with /c use <id>.`)}return m(`No machine matches "${e}". Send /c list to see ids and labels, or /c status to refresh the roster.`)}function Jd(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 Qe(l,e)?m("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 Qe(a,e)?fw(e):null}let s=ft(at());if(r==="use"||r==="bind"){let a=o.slice(1).join(" ").trim();if(!n){let y=ke();return Qe(y,e)?m("/c use is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}if(!a){let y=ke();return Qe(y,e)?m("Usage: /c use <label-or-id>"):null}e.clusterEnabled||Vr(!0);let c=ke().senderBindings[n]??null,{state:u,resolved:d}=za(n,a,"chat");if(!d.ok)return Qe(u,e)?hw(a,d):null;let p=d.peer.nodeId===s,f=c?c.nodeId===s:!1;if(!p)return null;let h=[`*Bound to ${d.peer.label}*`,`id \`${d.peer.nodeId}\``,"","Your messages now route to this machine. Other senders are not affected.","",gs(u,e,n)].join(`
|
|
106
|
+
`),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.","",Ba(u,e,n)].join(`
|
|
107
|
+
`);return ye(h,g)}if(r==="here"||r==="take"){if(!n){let d=ke();return Qe(d,e)?m("/c here is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}e.clusterEnabled||Vr(!0);let{state:a,resolved:l}=za(n,s,"chat");if(!l.ok)return Qe(a,e)?m("Could not bind to this machine."):null;let c=["*Bound to this machine.*","","Your messages now route here. Other senders are not affected.","",gs(a,e,n)].join(`
|
|
108
|
+
`),u=["<b>Bound to this machine.</b>","","Your messages now route here. Other senders are not affected.","",Ba(a,e,n)].join(`
|
|
109
|
+
`);return ye(c,u)}if(r==="using"){if(!n){let c=ke();return Qe(c,e)?m("/c using is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}let a=Bt(e,n);if(a){if(a.nodeId!==s)return null;let d=[...ke().peers,ir(e)].find(h=>h.nodeId===a.nodeId)?.label??"(unknown label)",p=[`*Bound to ${d}*`,`id \`${a.nodeId}\` \xB7 source ${a.source}`,a.source==="chat"?`since ${a.sinceIso}`:"(from config)"].join(`
|
|
110
|
+
`),f=[`<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(`
|
|
111
|
+
`);return ye(p,f)}let l=ke();return Qe(l,e)?m("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 p=ke();return Qe(p,e)?m("/c unuse is only available on chats with a known sender (allowlisted WhatsApp/Telegram)."):null}let l=ke().senderBindings[n]??null,{state:c}=pw(n);if(l){if(l.nodeId!==s)return null}else if(!Qe(c,e))return null;let u=Bt(e,n),d=u?`
|
|
108
112
|
Config default still applies: \`${u.nodeId}\`.`:`
|
|
109
|
-
No config default is set; nobody will answer until you /c use <label-or-id>.`;return
|
|
113
|
+
No config default is set; nobody will answer until you /c use <label-or-id>.`;return m(`Cleared your chat binding.${d}`)}if(r==="step-down"||r==="stepdown"){let a=ke();return Qe(a,e)?m("/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=Ka(e,n);return ye(a.wa,a.tg)}if(r==="list"){let a=ke(),l=n?Bt(e,n):null;if(l){if(l.nodeId!==s)return null}else if(!Qe(a,e))return null;return ye(gs(a,e,n??null),Ba(a,e,n??null))}let i=ke();return Qe(i,e)?m(`Unknown /c subcommand "${r}". Try /c help`):null}function qd(e,t){return Vr(!0),za(e,t,"chat")}ot();xe();function gw(e){return`wa:${e}`}function Ya(e){return`tg:${e}`}function Kd(e){return{peerKey:gw(e.fromJid),text:e.text,waMessageId:e.messageId,mediaSavedPath:e.mediaSavedPath,mediaError:e.mediaError}}j();function ys(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
114
|
`),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
115
|
`),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
116
|
`);return process.platform==="linux"?r:process.platform==="darwin"?s:process.platform==="win32"?i:[r,"","---","",s].join(`
|
|
113
|
-
`)}
|
|
117
|
+
`)}ce();import yt from"node:process";j();function ws(){let e=Xn,t=dt,n=["Linux (package manager or omnish pull install):",""," omnish pull install"," omnish pull install --whisper # needs python3 + venv"," omnish pull install --transcribe-node # optional Transformers.js (no Python)","","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
118
|
`),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
119
|
`),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
120
|
`);return process.platform==="linux"?n:process.platform==="darwin"?o:process.platform==="win32"?r:[n,"","---","",o,"","---","",r].join(`
|
|
117
|
-
`)}
|
|
118
|
-
`;return new Promise(r=>{let s=!1,i="";function a(c){s||(s=!0,r(c))}let l=
|
|
119
|
-
`);if(u>=0){let d=i.slice(0,u).trim();try{let
|
|
120
|
-
`)[0]?.trim()||void 0}async function
|
|
121
|
-
${(
|
|
121
|
+
`)}ce();import Pb from"node:path";j();import yw from"node:net";import ww from"node:fs";function bw(){try{let e=ww.readFileSync(Vn,"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 Mt(e){let t=bw();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)}
|
|
122
|
+
`;return new Promise(r=>{let s=!1,i="";function a(c){s||(s=!0,r(c))}let l=yw.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(`
|
|
123
|
+
`);if(u>=0){let d=i.slice(0,u).trim();try{let p=JSON.parse(d);p.ok?a(null):a(p.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 Yd(e,t,n){return`Step ${e+1}/${t}: ${n}\u2026`}function cn(){return{async stepStart(){},async stepFail(){}}}async function Qd(e,t){let n=e.peerKey?.trim();if(n){if(e.sendToPeer){try{await e.sendToPeer(n,t)}catch{}return}Mt({op:"sendPeerText",peerKey:n,text:t}).catch(()=>{})}}function bs(e){return!e.enabled||!e.peerKey?.trim()?cn():{async stepStart(t,n,o){await Qd(e,Yd(t,n,o.label))},async stepFail(t){await Qd(e,`Step failed: ${String(t)}`)}}}function Vd(){return process.env.OMNISH_PEER_KEY?.trim()||null}import vs from"node:fs";import lr from"node:path";j();import ks from"node:fs";import Ve from"node:path";var kw=160;function Xd(e){let t=Ve.basename(e.replace(/\\/g,"/")),n=Ve.extname(t);return{stem:n?t.slice(0,-n.length):t,ext:n.toLowerCase().slice(0,20)}}function ar(e){let{stem:t,ext:n}=Xd(e),o=t.normalize("NFKD").replace(new RegExp("\\p{M}","gu"),"").replace(/\s+/g,"_").replace(/[^A-Za-z0-9._-]+/g,"_").replace(/_+/g,"_").replace(/^[_.]+|[_.]+$/g,"").slice(0,kw);return o||(o="download"),`${o}${n}`}function Pn(e,t,n){U(e);let o=ar(t),r=n?Ve.resolve(n):null,s=Ve.join(e,o);if(!ks.existsSync(s)||Ve.resolve(s)===r)return s;let{stem:i,ext:a}=Xd(o);for(let l=1;l<1e4;l+=1){let c=`${i}-${l}${a}`;if(s=Ve.join(e,c),!ks.existsSync(s)||Ve.resolve(s)===r)return s}return Ve.join(e,`${i}-${Date.now()}${a}`)}function Qa(e,t){let n=Ve.resolve(e);if(!ks.existsSync(n))return n;let o=Ve.dirname(n),r=Ve.basename(n),s=Ve.extname(n).toLowerCase(),i=t?`${ar(t)}${s}`:ar(r);if(i===r)return n;let a=Pn(o,i,n);return Ve.resolve(a)===n?n:(ks.renameSync(n,a),Ve.resolve(a))}function En(e,t,n){let o=[],r=new Set,s=n&&t.length===1?n:void 0;for(let i of t){if(!i)continue;let a=Qa(i,s);r.has(a)||(r.add(a),o.push(a))}return o}import{spawn as vw}from"node:child_process";function po(e,t,n){return new Promise((o,r)=>{let s=vw(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 Zd(e){return lr.join(e,"%(title).200B.%(ext)s")}function ep(e,t){let n=[],o=r=>{let s;try{s=vs.readdirSync(r,{withFileTypes:!0})}catch{return}for(let i of s){let a=lr.join(r,i.name);if(i.isDirectory())o(a);else if(i.isFile())try{vs.statSync(a).mtimeMs>=t-2e3&&n.push(a)}catch{}}};return o(e),n.sort()}async function Sw(e,t){let n=await po(e,["--no-download","--print","title",t],6e4);if(n.code===0)return n.stdout.trim().split(`
|
|
124
|
+
`)[0]?.trim()||void 0}async function jt(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");vs.mkdirSync(s,{recursive:!0});let a=Date.now(),l=Zd(s),c=Math.min(9e5,Math.max(6e4,t.syncTimeoutMs*3)),u=await Sw(i,o),d=[],p=[],f=[];t.mediaMaxBytes>0&&f.push("--max-filesize",String(Math.floor(t.mediaMaxBytes)));let h=async k=>{let x=await po(i,k,c);if(p.push(x.stderr.trim()||x.stdout.trim()),x.code!==0)throw new Error(`yt-dlp failed (exit ${x.code}):
|
|
125
|
+
${(x.stderr||x.stdout).slice(-2e3)}`)},g=r==="all"?["video","audio","subs"]:[r];for(let k of g){let x=Date.now();if(k==="video"){if(!n.ffmpeg)throw new Error("ffmpeg required for video. Run: omnish pull install");await h(["-f","bv*+ba/b","--merge-output-format","mp4","--restrict-filenames","--ffmpeg-location",lr.dirname(n.ffmpeg),...f,"-o",l,o])}else if(k==="audio"){let T=n.ffmpeg?["--ffmpeg-location",lr.dirname(n.ffmpeg)]:[];await h(["-x","--audio-format","m4a","--restrict-filenames",...T,...f,"-o",l,o])}else k==="subs"&&await h(["--skip-download","--write-subs","--write-auto-subs","--sub-langs","en.*,en","--restrict-filenames","-o",l,o]);d.push(...ep(s,x))}let y=[...new Set(d)],b=En(s,y,u);return{title:u,outputDir:s,files:b.length>0?b:y,log:p.join(`
|
|
122
126
|
---
|
|
123
|
-
`)}}async function
|
|
124
|
-
`);return{lines:n,text:o}}async function
|
|
127
|
+
`)}}async function tp(e){let{tools:t,url:n,outputDir:o,timeoutMs:r}=e,s=t.ytDlp;if(!s)throw new Error("yt-dlp not found.");vs.mkdirSync(o,{recursive:!0});let i=Zd(o),a=Date.now(),l=t.ffmpeg?["--ffmpeg-location",lr.dirname(t.ffmpeg)]:[],c=await po(s,["-f","ba/b","-x","--audio-format","m4a","--restrict-filenames",...l,"-o",i,n],r);if(c.code!==0)throw new Error(`yt-dlp audio failed: ${(c.stderr||c.stdout).slice(-1500)}`);let u=ep(o,a).filter(d=>/\.(m4a|mp3|opus|webm|wav)$/i.test(d));if(u.length===0)throw new Error("No audio file produced.");return En(o,u)[0]??u[0]}j();Ss();import{spawnSync as el}from"node:child_process";import de from"node:fs";import Pw from"node:os";import ze from"node:path";var Ew="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp",Aw="https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe";function ap(){return process.platform==="win32"?"yt-dlp.exe":"yt-dlp"}function lp(){return process.platform==="win32"?"ffmpeg.exe":"ffmpeg"}function Xa(){return process.platform==="win32"?"whisper.exe":"whisper"}function fo(e){return ze.join(Xn,e)}function Za(){return process.platform==="win32"?ze.join(dt,"Scripts","whisper.exe"):ze.join(dt,"bin","whisper")}function Ln(e){try{return!de.existsSync(e)||!de.statSync(e).isFile()?!1:(process.platform==="win32"||de.accessSync(e,de.constants.X_OK),!0)}catch{return!1}}function In(e){let t=process.platform==="win32"?"where":"which",n=el(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&&Ln(o)?o:null}function ip(e,t,n){let o=e.trim();if(o&&Ln(o))return o;let r=fo(t);if(Ln(r))return r;for(let s of n){let i=In(s);if(i)return i}return null}function tt(e){let t=ip(e.pullYtDlpPath,ap(),["yt-dlp","yt-dlp.exe"]),n=ip(e.pullFfmpegPath,lp(),["ffmpeg","ffmpeg.exe"]),o=e.pullWhisperPath.trim(),r=null;o&&Ln(o)?r=o:Ln(fo(Xa()))?r=fo(Xa()):Ln(Za())?r=Za():r=In(process.platform==="win32"?"whisper.exe":"whisper");let s=process.platform==="win32"?In("python")??In("py"):In("python3")??In("python");return{ytDlp:t,ffmpeg:n,whisper:r,python:s}}function Cs(e){let t=tt(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:"transformers.js",ok:dn(),detail:dn()?`${An()} (cache: ${qo})`:"missing \u2014 optional: omnish pull install --transcribe-node"},{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(`
|
|
128
|
+
`);return{lines:n,text:o}}async function xs(e,t){U(ze.dirname(t));let n=await fetch(e,{redirect:"follow"});if(!n.ok)throw new Error(`Download failed (${n.status}): ${e}`);let o=Buffer.from(await n.arrayBuffer());de.writeFileSync(t,o),process.platform!=="win32"&&de.chmodSync(t,493)}function mo(e,t,n={}){if(el(e,t,{stdio:"inherit",cwd:n.cwd,timeout:6e5,windowsHide:!0}).status!==0)throw new Error(`Command failed: ${e} ${t.join(" ")}`)}async function Iw(){let e=fo(ap()),t=process.platform==="win32"?Aw:Ew;return await xs(t,e),e}async function Lw(){let e=fo(lp());U(Xn);let t=ze.join(D,"tmp","pull-install");if(U(t),process.platform==="win32"){let l="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip",c=ze.join(t,"ffmpeg.zip");await xs(l,c),mo("powershell",["-NoProfile","-Command",`Expand-Archive -Force -Path '${c.replace(/'/g,"''")}' -DestinationPath '${t.replace(/'/g,"''")}'`],{cwd:t});let d=de.readdirSync(t,{withFileTypes:!0}).find(f=>f.isDirectory()&&f.name.startsWith("ffmpeg"))?.name;if(!d)throw new Error("Could not find ffmpeg folder in archive.");let p=ze.join(t,d,"bin","ffmpeg.exe");if(!de.existsSync(p))throw new Error("ffmpeg.exe not found in archive.");return de.copyFileSync(p,e),e}if(process.platform==="darwin"){let l="https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip",c=ze.join(t,"ffmpeg.zip");await xs(l,c),mo("unzip",["-o",c,"-d",t],{cwd:t});let u=ze.join(t,"ffmpeg");if(!de.existsSync(u))throw new Error("ffmpeg binary not found after unzip.");return de.copyFileSync(u,e),de.chmodSync(e,493),e}let n=Pw.arch();if(n!=="x64"&&n!=="amd64")throw new Error(`Automatic ffmpeg install is not supported on linux/${n}. Install ffmpeg via your package manager and ensure it is on PATH.`);let o="https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz",r=ze.join(t,"ffmpeg.tar.xz");await xs(o,r),mo("tar",["-xf",r,"-C",t],{cwd:t});let i=de.readdirSync(t).filter(l=>l.startsWith("ffmpeg"))[0];if(!i)throw new Error("Could not find ffmpeg directory in tarball.");let a=ze.join(t,i,"ffmpeg");if(!de.existsSync(a))throw new Error("ffmpeg binary not found in tarball.");return de.copyFileSync(a,e),de.chmodSync(e,493),e}function Ow(){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(el(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 Nw(){U(ze.dirname(dt));let e=Ow();if(de.existsSync(dt))try{de.rmSync(dt,{recursive:!0,force:!0})}catch{}mo(e.cmd,[...e.args,"-m","venv",dt]);let t=process.platform==="win32"?ze.join(dt,"Scripts","pip.exe"):ze.join(dt,"bin","pip");mo(t,["install","-U","pip","openai-whisper"]);let n=Za();if(!Ln(n))throw new Error("Whisper install finished but whisper executable was not found.");let o=fo(Xa());try{de.existsSync(o)&&de.unlinkSync(o),de.symlinkSync(n,o)}catch{de.copyFileSync(n,o),process.platform!=="win32"&&de.chmodSync(o,493)}return n}function Fw(){let e=In(process.platform==="win32"?"npm.cmd":"npm");if(e)return e;throw new Error("npm not found. Install Node.js/npm then run: omnish pull install --transcribe-node")}async function _w(){U(Zn),U(qo);let e=ze.join(Zn,"package.json");de.existsSync(e)||de.writeFileSync(e,JSON.stringify({name:"omnish-transcribe",private:!0,type:"module",dependencies:{"@xenova/transformers":"^2.17.2"}},null,2),"utf8");let t=Fw();if(mo(t,["install","--omit=dev"],{cwd:Zn}),!dn())throw new Error("Transformers.js install finished but @xenova/transformers was not found.");return An()}async function Rs(e){let t=[],o=2+((e.whisper?1:0)+(e.transcribeNode?1:0)),r=0;e.progress&&await e.progress.stepStart(r++,o,"Installing yt-dlp");let s=await Iw();t.push(`yt-dlp \u2192 ${s}`);let i;e.progress&&await e.progress.stepStart(r++,o,"Installing ffmpeg");try{i=await Lw(),t.push(`ffmpeg \u2192 ${i}`)}catch(c){t.push(`ffmpeg: ${String(c)} (install manually; video merge needs it)`)}let a;e.whisper&&(e.progress&&await e.progress.stepStart(r++,o,"Installing whisper"),a=await Nw(),t.push(`whisper \u2192 ${a}`));let l;return e.transcribeNode&&(e.progress&&await e.progress.stepStart(r++,o,"Installing Transformers.js"),l=await _w(),t.push(`transformers.js \u2192 ${l}`)),{ytDlp:s,ffmpeg:i,whisper:a,transcribeNode:l,messages:t}}var Ww=/^https?:\/\/\S+$/i,Dw=/[.,;:!?)}\]"']+$/;function tl(e){let t=e.trim();if(!t||(t=t.replace(/^["']+|["']+$/g,""),t=t.replace(Dw,""),!Ww.test(t)))return null;try{return He(t),t}catch{return null}}function Ts(e){return tl(e)!==null}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}import Ms from"node:fs";import yp from"node:path";import mp from"node:fs";function cr(){let e=process.env.PLATFORM_REPLY_FILE_MAX_BYTES;if(e===void 0||e==="")return 0;let t=Number(e);return t===0?0:Number.isFinite(t)&&t>0?Math.floor(t):0}function cp(){let e=Number(process.env.PLATFORM_INBOUND_MEDIA_MAX_BYTES);return Number.isFinite(e)&&e>0?Math.floor(e):52*1024*1024}var aM=cp();function up(){let e=cp(),t=cr(),n=Math.max(e,t>0?t:e);return Math.ceil(n*1.4)+512*1024}var dp=50;function pp(e){try{return JSON.parse(e)}catch{return null}}var ol="OMNISH_ATTACHED_GATEWAY";function fp(){return process.env[ol]==="1"}function Nn(e){return fp()?sl(e):e.fileSendMaxBytes}function rl(e,t){return t?sl(e):Nn(e)}function On(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KiB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MiB`:`${(e/(1024*1024*1024)).toFixed(2)} GiB`}function hp(e){if(fp()){let t=sl(e),n=cr();return t<=0?"outbound size cap":e.fileSendMaxBytes>0&&n>0&&e.fileSendMaxBytes<n?`fileSendMaxBytes (${On(e.fileSendMaxBytes)}) and attached platform hop (${On(t)})`:n>0?`attached platform hop (max ${On(t)} per file)`:`fileSendMaxBytes (max ${On(t)} per file)`}return e.fileSendMaxBytes>0?`fileSendMaxBytes (max ${On(e.fileSendMaxBytes)} per file)`:"outbound size cap"}function sl(e){let t=cr();return t<=0?e.fileSendMaxBytes>0?e.fileSendMaxBytes:0:e.fileSendMaxBytes<=0?t:Math.min(e.fileSendMaxBytes,t)}function nl(e){let t=mp.statSync(e.absPath),n=cr();if(n>0&&t.size>n)throw new Error(`File too large for attached mode (max ${n} bytes).`);let o=mp.readFileSync(e.absPath).toString("base64");return{name:e.displayName,mimetype:e.mimetype,category:e.category,dataBase64:o,...e.caption?{caption:e.caption}:{}}}function $s(e,t,n,o){if(t.kind==="texts")return{body:t.bodies.map(s=>ue(s,o).text).filter(s=>s.trim()).join(`
|
|
125
129
|
|
|
126
|
-
`),...n?{messageId:n}:{}};if(t.kind==="text")return{body:
|
|
130
|
+
`),...n?{messageId:n}:{}};if(t.kind==="text")return{body:ue(t.body,o).text,...n?{messageId:n}:{}};if(t.kind==="bundle"){let r=(t.texts??[]).map(i=>ue(i,o).text).filter(i=>i.trim()),s=(t.files??[]).map(nl);return{body:r.length?r.join(`
|
|
127
131
|
|
|
128
|
-
`):void 0,...n?{messageId:n}:{},...s.length?{files:s}:{}}}return t.kind==="file"?{...n?{messageId:n}:{},files:[
|
|
129
|
-
|
|
132
|
+
`):void 0,...n?{messageId:n}:{},...s.length?{files:s}:{}}}return t.kind==="file"?{...n?{messageId:n}:{},files:[nl(t.spec)]}:{...n?{messageId:n}:{},files:t.specs.map(nl)}}var il=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"]),Uw=new Set(["html","htm","php","asp","aspx","jsp","cgi"]);function ur(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 gp(e){let t=typeof e=="string"?He(e):e,n=ur(t);return!n||Uw.has(n)?!1:il.has(n)}var Hw="omnish/1.0 (+https://omnish.dev)";function Bw(e){let t=[];if(e.mediaMaxBytes>0&&t.push(e.mediaMaxBytes),e.mediaSendFiles){let n=Nn(e);n>0&&t.push(n)}return t.length===0?0:Math.min(...t)}function Ps(e){let t=ar(e);return(!t||t==="download")&&!yp.basename(e.replace(/\\/g,"/")).replace(/[^A-Za-z0-9._-]+/g,"").length?"":t}function jw(e){if(!e)return null;let t=/filename\*\s*=\s*[^']*'[^']*'([^;]+)/i.exec(e);if(t?.[1])try{return Ps(decodeURIComponent(t[1].trim()))}catch{return Ps(t[1].trim())}let n=/filename\s*=\s*("([^"]+)"|([^;\s]+))/i.exec(e),o=n?.[2]??n?.[3];return o?Ps(o.trim()):null}function Gw(e,t){let n=jw(t);if(n)return n;let o=Ps(decodeURIComponent(e.pathname.split("/").pop()??""));if(o)return o;let r=ur(e);return`download-${Date.now()}${r?`.${r}`:""}`}async function wp(e){let t=He(e.url),n=Bw(e.cfg),o=await fetch(t.href,{redirect:"follow",headers:{"User-Agent":Hw,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=Gw(t,o.headers.get("content-disposition"));Ms.mkdirSync(e.outputDir,{recursive:!0});let i=Pn(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:p}=await c.read();if(d)break;if(p?.length){if(u+=p.length,n>0&&u>n)throw await c.cancel().catch(()=>{}),new Error(`File too large (max ${n} bytes).`);await new Promise((f,h)=>{l.write(Buffer.from(p),g=>g?h(g):f())})}}}catch(d){l.close();try{Ms.unlinkSync(i)}catch{}throw d}if(await new Promise((d,p)=>{l.end(f=>f?p(f):d())}),u===0){try{Ms.unlinkSync(i)}catch{}throw new Error("Download returned no data.")}return yp.resolve(i)}async function Gt(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 Yw from"node:fs";import bp from"node:fs";import kp from"node:path";var zw=new Set(["jpg","jpeg","png","gif","webp","bmp","tif","tiff","heic","avif"]),Jw=new Set(["mp4","mov","webm","mkv","avi","m4v","3gp"]),qw=new Set(["mp3","ogg","opus","wav","m4a","flac","aac","wma"]),Es={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 Kw(e){let t=e.replace(/^\./,"").toLowerCase();return zw.has(t)?{category:"image",mimetype:Es[t]??"image/jpeg"}:Jw.has(t)?{category:"video",mimetype:Es[t]??"video/mp4"}:qw.has(t)?{category:"audio",mimetype:Es[t]??"audio/mpeg"}:{category:"document",mimetype:Es[t]??"application/octet-stream"}}function ht(e,t){let n;try{n=bp.realpathSync(e)}catch{return{error:"File not found or unreadable."}}let o;try{o=bp.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=kp.basename(n),s=kp.extname(n).slice(1),{category:i,mimetype:a}=Kw(s);return{absPath:n,category:i,mimetype:a,displayName:r}}function Qw(e,t){let n=Nn(e),o=[],r=[];for(let s of t){let i=ht(s,n);if("error"in i){let a=0;try{a=Yw.statSync(s).size}catch{}r.push({absPath:s,sizeBytes:a});continue}o.push(i)}return{specs:o,skipped:r}}function Vw(e,t){let n=hp(e),o=t.map(r=>` ${r.absPath} (${On(r.sizeBytes)})`);return`${t.length} file(s) not sent (${n}):
|
|
133
|
+
${o.join(`
|
|
134
|
+
`)}`}function pn(e){let{cfg:t,files:n,textLines:o,skippedNote:r}=e,s=n.filter(c=>typeof c=="string"&&c.length>0),i=[...o];if(r&&i.push(r),!t.mediaSendFiles){let c=s.length?s.map(u=>` ${u}`):[" (no files)"];return{kind:"text",body:m([...i,"Files:",...c].filter(Boolean).join(`
|
|
135
|
+
`))}}let{specs:a,skipped:l}=Qw(t,s);return l.length>0&&i.push(Vw(t,l)),a.length===0?{kind:"text",body:m(i.join(`
|
|
130
136
|
|
|
131
|
-
`)||"(done)")}:{kind:"bundle",texts:i.length?i.map(
|
|
137
|
+
`)||"(done)")}:{kind:"bundle",texts:i.length?i.map(c=>m(c)):void 0,files:a}}import vp from"node:fs";var Xw="omnish/1.0 (+https://omnish.dev)",Sp=2*1024*1024,xp=48e3;function Zw(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 ho(e){return e.replace(/ /gi," ").replace(/&/gi,"&").replace(/</gi,"<").replace(/>/gi,">").replace(/"/gi,'"').replace(/'/gi,"'")}function eb(e){let t=Zw(e);t=t.replace(/<title[^>]*>([\s\S]*?)<\/title>/gi,(n,o)=>`# ${ho(o.trim())}
|
|
132
138
|
|
|
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)} ${
|
|
139
|
+
`);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)} ${ho(s.replace(/<[^>]+>/g,"").trim())}
|
|
134
140
|
|
|
135
|
-
`)}return t=t.replace(/<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,(n,o,r)=>`[${
|
|
136
|
-
`),t=t.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi,(n,o)=>`${
|
|
141
|
+
`)}return t=t.replace(/<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,(n,o,r)=>`[${ho(r.replace(/<[^>]+>/g,"").trim())||o}](${o})`),t=t.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi,(n,o)=>`- ${ho(o.replace(/<[^>]+>/g,"").trim())}
|
|
142
|
+
`),t=t.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi,(n,o)=>`${ho(o.replace(/<[^>]+>/g,"").trim())}
|
|
137
143
|
|
|
138
144
|
`),t=t.replace(/<br\s*\/?>/gi,`
|
|
139
|
-
`),t=t.replace(/<[^>]+>/g,""),t=
|
|
145
|
+
`),t=t.replace(/<[^>]+>/g,""),t=ho(t),t=t.replace(/\n{3,}/g,`
|
|
140
146
|
|
|
141
|
-
`).trim(),t.length>
|
|
147
|
+
`).trim(),t.length>xp&&(t=`${t.slice(0,xp)}
|
|
142
148
|
|
|
143
|
-
\u2026 (truncated)`),t}async function
|
|
144
|
-
`).pop()?.trim()??"";return i?
|
|
149
|
+
\u2026 (truncated)`),t}async function Cp(e){let t=He(e.url),n=await fetch(t.href,{redirect:"follow",headers:{"User-Agent":Xw,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>Sp)throw await o.cancel().catch(()=>{}),new Error(`Page too large (max ${Sp} bytes).`);r.push(u)}}let i=Buffer.concat(r).toString("utf8"),a=eb(i);if(!a.trim())throw new Error("Could not extract readable content from page.");let l;if(a.length>12e3){let c=`${t.hostname.replace(/\./g,"-")}.md`;l=Pn(e.outputDir,c),vp.mkdirSync(e.outputDir,{recursive:!0}),vp.writeFileSync(l,a,{encoding:"utf8",mode:384})}return{markdown:a,savedPath:l}}function Rp(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 tb from"node:fs";import Tp from"node:path";function mn(e,t,n){let o=e.mediaOutputDir.trim(),r;return o?r=Tp.isAbsolute(o)?o:Tp.resolve(t,o):n&&is(n)==="sessionCwd"?r=t:r=ls(),tb.mkdirSync(r,{recursive:!0}),r}var nb=2e4,ob=new Set(["generic","genericembed","htm5","html5","html5media","mime"]);function rb(e){let t=e.trim().toLowerCase();return!t||ob.has(t)}async function $p(e,t,n=nb){let r=tt(e).ytDlp;if(!r)return{supported:!1,reason:"no_tool"};try{let s=await po(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(`
|
|
150
|
+
`).pop()?.trim()??"";return i?rb(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 sb=[/^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],ib="omnish/1.0 (+https://omnish.dev)";function ab(e){let t=e.pathname.toLowerCase();if(/\/pdf(?:\/|$)/i.test(t)||/\/download(?:\/|$)/i.test(t))return!0;let n=ur(e);return!!(n&&il.has(n))}function lb(e){if(!e)return!1;let t=e.split(";")[0]?.trim()??"";return sb.some(n=>n.test(t))}function cb(e){if(!e)return!1;let t=e.split(";")[0]?.trim().toLowerCase()??"";return t==="text/html"||t==="application/xhtml+xml"}async function ub(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":ib,Accept:"*/*"},signal:o.signal})).headers.get("content-type")}catch{return null}finally{clearTimeout(r)}}async function Mp(e,t,n={}){if(n.force==="file")return"file";if(n.force==="video")return"video";let o=He(t);if(gp(o)||ab(o))return"file";let r=n.allowHead!==!1,s=null;return r&&(s=await ub(t,5e3),lb(s))?"file":(await $p(e,t)).supported?"video":cb(s)||s===null&&r?"html":"file"}async function Pp(e){He(e.url);let t=mn(e.cfg,e.sessionCwd,e.peerKey),n=e.reporter??cn(),o=await Mp(e.cfg,e.url,e.classify??{});if(o==="file"){let[c]=await Gt(n,[{label:"Downloading file",run:async()=>wp({cfg:e.cfg,url:e.url,outputDir:t})}]),u=En(t,[c]),d=u[0]??c;return pn({cfg:e.cfg,files:u,textLines:[`Saved: ${d}`,`Output: ${t}`]})}if(o==="html"){let[c]=await Gt(n,[{label:"Fetching page",run:async()=>Cp({url:e.url,outputDir:t})}]),u=Math.min(e.cfg.appsMaxWaChars||3500,4e3),d=Rp(c.markdown,u),p=[`Source: ${e.url}`];c.savedPath&&p.push(`Saved: ${c.savedPath}`);let f=d.map((h,g)=>m(g===0?`${p.join(`
|
|
145
151
|
`)}
|
|
146
152
|
|
|
147
|
-
${
|
|
148
|
-
${(
|
|
149
|
-
|
|
150
|
-
`);await
|
|
151
|
-
|
|
152
|
-
`);r&&await
|
|
153
|
-
|
|
153
|
+
${h}`:h));if(c.savedPath&&e.cfg.mediaSendFiles){let h=En(t,[c.savedPath]);return pn({cfg:e.cfg,files:h,textLines:p})}return{kind:"bundle",texts:f}}let r=tt(e.cfg);if(!r.ytDlp)return{kind:"text",body:m("yt-dlp missing. Run: omnish pull install")};if(!r.ffmpeg)return{kind:"text",body:m("ffmpeg missing. Run: omnish pull install")};let[s]=await Gt(n,[{label:"Downloading video",run:async()=>jt({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=En(t,i.length?i:s.files,s.title),l=[s.title?`Title: ${s.title}`:"",`Saved under: ${t}`,...e.cfg.mediaSendFiles&&a.length?["Files:",...a.map(c=>` ${c}`)]:[]].filter(Boolean);return pn({cfg:e.cfg,files:a,textLines:l})}import{spawnSync as db}from"node:child_process";import pb from"node:fs";import dr from"node:path";import Ep from"node:fs";import Ap from"node:path";function As(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=Ap.isAbsolute(n)?n:Ap.resolve(t,n);if(!Ep.existsSync(o))throw new Error(`File not found: ${o}`);if(!Ep.statSync(o).isFile())throw new Error(`Not a file: ${o}`);return{kind:"file",absPath:o}}function go(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 Ip(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=go(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=go(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=go(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}if(/^--audio-only\b/i.test(r)){n.audioOnly=!0,t=r.replace(/^--audio-only\s*/i,"").trim(),o=!0;continue}let c=r.match(/^([\s\S]+?)\s+(?:--from|--start|-ss)\s+(\S+)\s*$/i);if(c){n.fromSec=go(c[2]),t=c[1].trim(),o=!0;continue}let u=r.match(/^([\s\S]+?)\s+(?:--to|--end)\s+(\S+)\s*$/i);if(u){n.toSec=go(u[2]),t=u[1].trim(),o=!0;continue}let d=r.match(/^([\s\S]+?)\s+(?:-t|--duration)\s+(\S+)\s*$/i);if(d){n.durationSec=go(d[2]),t=d[1].trim(),o=!0;continue}let p=r.match(/^([\s\S]+?)\s+(?:--format|-f)\s+(\S+)\s*$/i);if(p){n.format=p[2].replace(/^\./,"").toLowerCase(),t=p[1].trim(),o=!0;continue}let f=r.match(/^([\s\S]+?)\s+--audio-only\s*$/i);f&&(n.audioOnly=!0,t=f[1].trim(),o=!0)}if(!t)throw new Error("Missing URL or file path.");return{targetRaw:t,opts:n}}function mb(e,t){return t.format?t.format.replace(/^\./,""):t.audioOnly?"m4a":dr.extname(e).replace(/^\./,"")||"mp4"}function fb(e,t,n){let o=dr.extname(e).toLowerCase(),r=dr.extname(t).toLowerCase(),s=["-y"];return n.fromSec!==null&&n.fromSec>0&&s.push("-ss",String(n.fromSec)),s.push("-i",e),n.durationSec!==null?s.push("-t",String(n.durationSec)):n.toSec!==null&&(n.fromSec!==null?s.push("-t",String(Math.max(.1,n.toSec-n.fromSec))):s.push("-to",String(n.toSec))),n.audioOnly||[".mp3",".m4a",".wav",".opus"].includes(r)?(s.push("-vn"),r===".mp3"?s.push("-acodec","libmp3lame"):r===".wav"&&s.push("-acodec","pcm_s16le")):o===r&&!n.audioOnly&&n.format===null&&s.push("-c","copy"),s.push(t),s}function hb(e,t,n,o,r){let s=fb(t,n,o),i=db(e,s,{encoding:"utf8",timeout:r,windowsHide:!0});if(i.status!==0)throw new Error(`ffmpeg failed:
|
|
154
|
+
${(i.stderr||i.stdout).slice(-2e3)}`);if(!pb.existsSync(n))throw new Error("ffmpeg did not produce output file.")}async function Lp(e){let t=tt(e.cfg),n=t.ffmpeg;if(!n)return{kind:"text",body:m("ffmpeg missing. Run: omnish pull install")};let{targetRaw:o,opts:r}=Ip(e.args),s=mn(e.cfg,e.sessionCwd,e.peerKey),i=e.reporter??cn(),a=As(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[h]=await Gt(i,[{label:"Downloading",run:async()=>{let g=await jt({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=h}else c=a.absPath;let u=mb(c,r),d=dr.basename(c,dr.extname(c)),p=Pn(s,`${d}-edit.${u}`),[f]=await Gt(i,[{label:"Editing",run:async()=>(hb(n,c,p,r,l),Qa(p))}]);return pn({cfg:e.cfg,files:[f],textLines:[`Edited: ${f}`]})}import Tb from"node:fs";import{spawnSync as _p}from"node:child_process";import pr from"node:fs";import gt from"node:path";Ss();function gb(e){let t=e.trim().toLowerCase()||"auto";return t==="cpu"?["cpu"]:t==="cuda"?["cuda"]:[void 0,"cpu"]}function yb(e,t,n,o){let r=[e,"--model",t,"--output_dir",n,"--output_format","all"];return o&&r.push("--device",o),r}function wb(e){return!!(/CUDA error:\s*out of memory/i.test(e)||/cuda.*out of memory/i.test(e)||/out of memory/i.test(e)&&/cuda/i.test(e))}function bb(e,t){let n=t?` (signal ${t})`:"";return`Process timed out after ${e}ms${n}`}function kb(e){let t=e.stderr.slice(-2e3),n=e.triedDevices.length>0?e.triedDevices.join(" \u2192 "):"(none)",o=['Try mediaWhisperDevice: "cpu" in config.json',"or a smaller mediaWhisperModel (tiny, base)."];e.timeoutMs&&o.push(`or increase mediaTranscribeTimeoutMs (currently ${e.timeoutMs} ms per attempt).`),o.push("For a Python-free fallback: omnish pull install --transcribe-node");let r=e.cpuFallbackAttempted?"CPU fallback after ffmpeg audio extract was attempted.":"";return[`whisper failed (devices tried: ${n}).`,r,...o,"",t].filter(Boolean).join(`
|
|
155
|
+
`)}function Wp(e,t){let n=(typeof e.stderr=="string"?e.stderr:"")||"",o=(typeof e.stdout=="string"?e.stdout:"")||"",r=e.error&&typeof e.error=="object"&&"code"in e.error?String(e.error.code):"",s=e.status===null&&r==="ETIMEDOUT";if(e.status===null){let i=bb(t,e.signal);n=n?`${n}
|
|
156
|
+
${i}`:i}return{status:e.status,stderr:n,stdout:o,signal:e.signal,timedOut:s||e.status===null}}function vb(e,t,n){let o=_p(e,t,{encoding:"utf8",timeout:n,windowsHide:!0});return Wp(o,n)}function Sb(e){return e??"default"}function Op(e,t){let n=gt.basename(t,gt.extname(t));return[".txt",".srt",".vtt",".json"].map(r=>gt.join(e,n+r)).filter(r=>pr.existsSync(r))}function Np(e,t,n){let o=gt.basename(t,gt.extname(t)),r=gt.basename(n,gt.extname(n));if(o===r)return Op(e,n);for(let s of[".txt",".srt",".vtt",".json"]){let i=gt.join(e,o+s),a=gt.join(e,r+s);if(pr.existsSync(i))try{pr.copyFileSync(i,a)}catch{}}return Op(e,n)}function xb(e,t,n,o,r=".m4a"){let s=gt.join(n,`_whisper_audio${r||gt.extname(t)||".m4a"}`),i=_p(e,["-y","-i",t,"-vn","-ar","16000","-ac","1",s],{encoding:"utf8",timeout:o,windowsHide:!0}),a=Wp(i,o);return i.status===0&&pr.existsSync(s)?s:(a.timedOut,null)}function Fp(e){let t=e.runWhisper??vb,n=gb(e.deviceConfig),o=e.deviceConfig.trim().toLowerCase()||"auto",r=[],s="";for(let i=0;i<n.length;i++){let a=n[i];r.push(Sb(a));let l=yb(e.inputPath,e.model,e.outputDir,a),c=t(e.whisper,l,e.timeoutMs);if(c.status===0)return{ok:!0,log:c.stderr||c.stdout||"",whisperInputPath:e.inputPath};if(s=c.stderr||c.stdout||"",!(i<n.length-1&&(o==="auto"||wb(s))))break}return{ok:!1,stderr:s,triedDevices:r}}function al(e){return Math.min(9e5,Math.max(6e4,e.mediaTranscribeTimeoutMs))}function Is(e,t){let n=e.mediaTranscribeEngine.trim().toLowerCase()||"whisper",o=!!t.whisper,r=dn();return n==="transformers"?r?null:"Transformers.js missing. Run: omnish pull install --transcribe-node":n==="whisper"?o?null:"whisper missing. Run: omnish pull install --whisper":!o&&!r?"No transcription backend. Run: omnish pull install --whisper and/or --transcribe-node":null}async function Cb(e){let{cfg:t,tools:n,inputPath:o,outputDir:r,runWhisper:s}=e,i=n.whisper;if(!i)throw new Error("whisper not found. Run: omnish pull install --whisper");if(!pr.existsSync(o))throw new Error(`File not found: ${o}`);let a=al(t),l=t.mediaWhisperModel.trim()||"small",c=t.mediaWhisperDevice.trim()||"auto",u=[],d="",p=!1,f=Fp({whisper:i,inputPath:o,model:l,outputDir:r,deviceConfig:c,timeoutMs:a,runWhisper:s});if(f.ok)return{files:Np(r,f.whisperInputPath,o),log:f.log};if(u=f.triedDevices,d=f.stderr,n.ffmpeg){let h=xb(n.ffmpeg,o,r,a);if(h){p=!0;let g=Fp({whisper:i,inputPath:h,model:l,outputDir:r,deviceConfig:"cpu",timeoutMs:a,runWhisper:s});if(g.ok)return{files:Np(r,g.whisperInputPath,o),log:g.log};u=[...u,...g.triedDevices.map(y=>`extract+${y}`)],d=g.stderr}}throw new Error(kb({stderr:d,triedDevices:u,cpuFallbackAttempted:p,timeoutMs:a}))}function Rb(e,t){let n=e.mediaTranscribeEngine.trim().toLowerCase()||"whisper";return n==="transformers"||n==="auto"&&t?!0:t&&e.mediaTranscribeFallback}async function ll(e){let{cfg:t,tools:n,inputPath:o,outputDir:r,runWhisper:s}=e,i=t.mediaTranscribeEngine.trim().toLowerCase()||"whisper",a=al(t),l=await Promise.resolve().then(()=>(Ss(),sp));if(i==="transformers")return l.transcribeWithTransformers({cfg:t,tools:n,inputPath:o,outputDir:r,timeoutMs:a});try{return await Cb({cfg:t,tools:n,inputPath:o,outputDir:r,runWhisper:s})}catch(c){if(!Rb(t,!0))throw c;if(!l.isNodeTranscribeInstalled()){let u=" Transformers.js fallback unavailable (run: omnish pull install --transcribe-node).";throw new Error(String(c)+u)}try{let u=await l.transcribeWithTransformers({cfg:t,tools:n,inputPath:o,outputDir:r,timeoutMs:a});return{files:u.files,log:`${u.log} (whisper failed; used fallback)`}}catch(u){throw new Error(`${String(c)}
|
|
157
|
+
|
|
158
|
+
transformers fallback failed: ${String(u)}`)}}}async function cl(e){let{cfg:t,tools:n,url:o,outputDir:r}=e,s=al(t),i=await tp({tools:n,url:o,outputDir:r,timeoutMs:s});return ll({cfg:t,tools:n,inputPath:i,outputDir:r})}function $b(e){try{return Tb.readFileSync(e,"utf8").trim()}catch{return""}}function Mb(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 Dp(e){let t=tt(e.cfg),n=Is(e.cfg,t);if(n)return{kind:"text",body:m(n)};let o=As(e.targetRaw,e.sessionCwd),r=mn(e.cfg,e.sessionCwd,e.peerKey),s=e.reporter??cn(),i="",a=null,l=[];if(o.kind==="url"){if(!t.ytDlp)return{kind:"text",body:m("yt-dlp missing. Run: omnish pull install")};if(!t.ffmpeg)return{kind:"text",body:m("ffmpeg missing. Run: omnish pull install")};l.push({label:"Downloading video",run:async()=>{let b=await jt({cfg:e.cfg,tools:t,url:o.url,mode:"video",outputDir:r});if(a=b.files.filter(x=>/\.(mp4|mkv|webm|mov)$/i.test(x))[0]??b.files[0]??null,!a)throw new Error("No video file downloaded.");return i=a,b}})}else i=o.absPath;l.push({label:"Transcribing",run:async()=>ll({cfg:e.cfg,tools:t,inputPath:i,outputDir:r})}),l.push({label:"Preparing results",run:async()=>{}});let c=await Gt(s,l),u=c[c.length-2],d=u.files.find(b=>b.endsWith(".txt")),p=u.files.find(b=>b.endsWith(".srt")||b.endsWith(".vtt")),f=d?$b(d):"",h=Math.min(e.cfg.appsMaxWaChars,e.cfg.syncMaxBytes)||3500,g=f?Mb(f,h):["(no transcript text produced)"],y=[];return p&&y.push(p),o.kind==="url"&&a&&y.push(a),pn({cfg:e.cfg,files:y,textLines:g})}async function yo(e,t){let n=await Mt({op:"sendPeerText",peerKey:e,text:t});if(n)throw new Error(n)}async function ul(e,t,n){let o=await Mt({op:"sendPeerMedia",peerKey:e,absPath:t,...n?{caption:n}:{}});if(o)throw new Error(o)}async function Up(e,t,n){let o=e.trim();if(o){if(t.kind==="text"){await yo(o,t.body.wa);return}if(t.kind==="file"){n.mediaSendFiles?await ul(o,t.spec.absPath,t.spec.caption):await yo(o,`Saved: ${t.spec.absPath}`);return}if(t.kind==="files"){if(!n.mediaSendFiles){let r=t.specs.map(s=>` ${s.absPath}`).join(`
|
|
159
|
+
`);await yo(o,`Files:
|
|
160
|
+
${r}`);return}for(let r of t.specs)await ul(o,r.absPath,r.caption);return}if(t.kind==="bundle"){for(let r of t.texts??[])await yo(o,r.wa);if(!n.mediaSendFiles){let r=(t.files??[]).map(s=>` ${s.absPath}`).join(`
|
|
161
|
+
`);r&&await yo(o,`Files:
|
|
162
|
+
${r}`);return}for(let r of t.files??[])try{await ul(o,r.absPath,r.caption)}catch{await yo(o,`Could not send: ${r.absPath}`)}}}}function Hp(e){if(e.kind==="text"){process.stdout.write(`${e.body.wa}
|
|
154
163
|
`);return}if(e.kind==="bundle"){for(let t of e.texts??[])process.stdout.write(`${t.wa}
|
|
155
164
|
|
|
156
165
|
`);for(let t of e.files??[])process.stdout.write(`FILE: ${t.absPath}
|
|
157
166
|
`);return}if(e.kind==="file"){process.stdout.write(`FILE: ${e.spec.absPath}
|
|
158
167
|
`);return}if(e.kind==="files")for(let t of e.specs)process.stdout.write(`FILE: ${t.absPath}
|
|
159
|
-
`)}async function
|
|
160
|
-
`)}}function
|
|
161
|
-
`),process.exitCode=1}
|
|
162
|
-
`)}}async function
|
|
163
|
-
`)}function
|
|
168
|
+
`)}async function dl(e,t,n){if(Hp(e),t)try{await Up(t,e,n)}catch(o){process.stderr.write(`Peer delivery failed: ${String(o)}
|
|
169
|
+
`)}}function Eb(e){let t=e.toLowerCase();return t==="dlf"?{force:"file"}:t==="dlv"?{force:"video"}:{}}async function pl(e){let[t,n,o]=e,r=(t??"").toLowerCase(),s=S(),i=o?.trim()?Pb.resolve(o):process.cwd(),a=Vd(),l=bs({peerKey:a,enabled:s.progressUpdates});if(r==="dl"||r==="dlf"||r==="dlv"){await dl(await Pp({cfg:s,url:n??"",sessionCwd:i,peerKey:a??void 0,reporter:l,classify:Eb(r)}),a,s);return}if(r==="tr"){await dl(await Dp({cfg:s,targetRaw:n??"",sessionCwd:i,peerKey:a??void 0,reporter:l}),a,s);return}if(r==="edit"){await dl(await Lp({cfg:s,args:n??"",sessionCwd:i,peerKey:a??void 0,reporter:l}),a,s);return}process.stderr.write(`Unknown media-exec command: ${r}
|
|
170
|
+
`),process.exitCode=1}ce();j();import Ab from"node:fs";import Ib from"node:path";function Lb(e,t){return mn(e,t)}async function Ob(e){He(e.url);let t=tt(e.cfg),n=Lb(e.cfg,e.sessionCwd),o=[],r;if(e.mode==="transcript"){let a=await cl({cfg:e.cfg,tools:t,url:e.url,outputDir:n});o.push(...a.files)}else if(e.mode==="all"){let a=await jt({cfg:e.cfg,tools:t,url:e.url,mode:"all",outputDir:n});r=a.title,o.push(...a.files);try{let l=await cl({cfg:e.cfg,tools:t,url:e.url,outputDir:n});o.push(...l.files)}catch{}}else{let a=await jt({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"&&Ab.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(`
|
|
171
|
+
`)}}async function Bp(e){let[t,n,o]=e,r=t??"audio",s=S(),i=o?.trim()?Ib.resolve(o):ia,a=await Ob({cfg:s,mode:r,url:n??"",sessionCwd:i});process.stdout.write(a.summary+`
|
|
172
|
+
`)}function ml(){let e=yt.stdout;console.log(Ce(e,"omnish pull"),w(e,"\u2014 download media from URLs (yt-dlp + ffmpeg + optional Whisper)")),console.log(""),console.log(q(e,"Usage:")),console.log(` ${v(e,"omnish pull doctor")}`),console.log(` ${v(e,"omnish pull install")} ${w(e,"[--whisper] [--transcribe-node]")}`),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 jp(e){let t=(e[0]??"").toLowerCase(),n=e.slice(1);if(!t||t==="help"||t==="-h"||t==="--help"){ml();return}if(t==="doctor"){let o=S(),{text:r}=Cs(o);console.log(H(yt.stdout,r));return}if(t==="instructions"||t==="setup"){console.log(ws());return}if(t==="install"){let o=n.some(i=>i==="--whisper"),r=n.some(i=>i==="--transcribe-node"),s=yt.stdout;console.log(H(s,"Installing pull tools into ~/.omnish/bin \u2026"));try{let i=await Rs({whisper:o,transcribeNode:r});for(let a of i.messages)console.log(H(s,a))}catch(i){console.error(R(yt.stderr,String(i))),yt.exitCode=1}return}console.error(R(yt.stderr,`Unknown subcommand "${t}". Try: omnish pull help`)),yt.exitCode=1}async function Gp(e){try{let t=(e[0]??"").toLowerCase();if(t==="dl"||t==="tr"||t==="edit"){await pl(e);return}await Bp(e)}catch(t){console.error(R(yt.stderr,String(t))),yt.exitCode=1}}async function zp(e){try{await pl(e)}catch(t){console.error(R(yt.stderr,String(t))),yt.exitCode=1}}j();import{execFileSync as fn}from"node:child_process";import wo from"node:fs";import fr from"node:os";import wt from"node:path";import Je from"node:process";function zt(){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=wt.resolve(t),o=Je.env.OMNISH_HOME?.trim(),r=o?wt.resolve(o):D;return{nodePath:e,scriptPath:n,omnishHome:r}}function Jp(e){return/[ "'\\\s]/.test(e)?`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`:e}function Nb(e){return`Environment="OMNISH_HOME=${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`}function Fb(e){return`[Unit]
|
|
164
173
|
Description=omnish gateway (WhatsApp/Telegram)
|
|
165
174
|
After=network-online.target
|
|
166
175
|
Wants=network-online.target
|
|
167
176
|
|
|
168
177
|
[Service]
|
|
169
178
|
Type=simple
|
|
170
|
-
ExecStart=${`${
|
|
171
|
-
${
|
|
179
|
+
ExecStart=${`${Jp(e.nodePath)} ${Jp(e.scriptPath)} run`}
|
|
180
|
+
${Nb(e.omnishHome)}
|
|
172
181
|
Restart=on-failure
|
|
173
182
|
RestartSec=5
|
|
174
183
|
|
|
175
184
|
[Install]
|
|
176
185
|
WantedBy=default.target
|
|
177
|
-
`}function
|
|
186
|
+
`}function mr(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function _b(e){let t=fr.homedir(),n=wt.join(e.omnishHome,"logs","launchd-stdout.log"),o=wt.join(e.omnishHome,"logs","launchd-stderr.log");U(wt.dirname(n));let r=mr(e.nodePath),s=mr(e.scriptPath),i=mr(e.omnishHome),a=mr(n),l=mr(o);return`<?xml version="1.0" encoding="UTF-8"?>
|
|
178
187
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
179
188
|
<plist version="1.0">
|
|
180
189
|
<dict>
|
|
@@ -201,40 +210,40 @@ WantedBy=default.target
|
|
|
201
210
|
<string>${l}</string>
|
|
202
211
|
</dict>
|
|
203
212
|
</plist>
|
|
204
|
-
`}function
|
|
213
|
+
`}function Ls(){let e=zt();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=wt.join(fr.homedir(),"Library/LaunchAgents/dev.omnish.gateway.plist");U(wt.dirname(t));let n=_b(e);wo.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{fn("launchctl",["bootout",r,t],{stdio:"pipe"})}catch{}return fn("launchctl",["bootstrap",r,t],{stdio:"inherit"}),fn("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=wt.join(fr.homedir(),".config","systemd","user");U(t);let n=wt.join(t,"omnish.service"),o=Fb(e);return wo.writeFileSync(n,o,{mode:420}),fn("systemctl",["--user","daemon-reload"],{stdio:"inherit"}),fn("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 Os(){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=wt.join(fr.homedir(),"Library/LaunchAgents/dev.omnish.gateway.plist"),n=`gui/${typeof Je.getuid=="function"?Je.getuid():0}`;if(wo.existsSync(e)){try{fn("launchctl",["bootout",n,e],{stdio:"pipe"})}catch{}wo.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=wt.join(fr.homedir(),".config","systemd","user","omnish.service");try{fn("systemctl",["--user","disable","--now","omnish.service"],{stdio:"pipe"})}catch{}wo.existsSync(e)&&wo.unlinkSync(e);try{fn("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 qp from"node:fs";var Ns=48e3;function Fs(e,t){try{if(!qp.existsSync(e))return"(no log file yet \u2014 start with `omnish run -d` or the systemd/LaunchAgent service.)";let n=qp.readFileSync(e),r=(n.length>Ns?n.subarray(n.length-Ns):n).toString("utf8");return n.length>Ns&&(r=`\u2026(truncated to last ${Ns} bytes)
|
|
205
214
|
${r}`),r.split(/\r?\n/).slice(-t).join(`
|
|
206
|
-
`).trimEnd()||"(empty)"}catch(n){return`Could not read log: ${String(n)}`}}import
|
|
207
|
-
`,{mode:384})}function
|
|
208
|
-
### `)}function
|
|
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
|
|
210
|
-
`).trim();if(!n)throw new Error("Recipe body is empty.");let o=n.match(/^--steps\s+([\s\S]+)$/i);if(o){let
|
|
211
|
-
${c}`}function
|
|
212
|
-
`)}
|
|
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
|
|
214
|
-
`,n++;continue}if(r==="t"){t+=" ",n++;continue}if(r==="\\"){t+="\\",n++;continue}}t+=o}return t}function
|
|
215
|
-
[\u2026+${
|
|
215
|
+
`).trimEnd()||"(empty)"}catch(n){return`Could not read log: ${String(n)}`}}import hn from"node:fs";import rk from"node:path";j();import Wb from"node:crypto";import gl from"node:fs";import Db from"node:path";var yl=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,ko="__omnish_recipes_global__",wl=new Set(["help","list","show","add","set","remove","rm","del","run","r","online"]),bo=new Map,Kp=!1;function Ub(){try{let e=gl.readFileSync(Br,"utf8"),t=JSON.parse(e);if(t&&typeof t=="object")for(let[n,o]of Object.entries(t)){if(!o||typeof o!="object")continue;let r={};for(let[s,i]of Object.entries(o)){let a=String(s).toLowerCase();i&&typeof i=="object"&&typeof i.command=="string"&&(r[a]=Xe(i))}bo.set(n,r)}}catch{}}function Hb(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 Xe(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=Hb(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 Ws(){U(Db.dirname(Br));let e={};for(let[t,n]of bo)Object.entries(n).length>0&&(e[t]={...n});gl.writeFileSync(Br,JSON.stringify(e,null,2)+`
|
|
216
|
+
`,{mode:384})}function Ds(){Kp||(Ub(),Kp=!0)}function Yp(e){return Ds(),bo.get(e)??{}}function fl(e){Ds();let t=bo.get(e);return t||(t={},bo.set(e,t)),t}function Bb(e,t){let n=e.taskEnv??"OMNISH_TASK",o=t.recipesMacroDefaultCommand.trim();return Fn(o,n).ok?Xe({...e,command:o,promptTemplate:e.command}):e}function jb(e,t){if(e.promptTemplate||Fn(e.command,t).ok||!So(e.command,t))return!1;let n=e.command;return n.includes("```")||n.length>2e3||/^"Create\b/i.test(n)||n.includes("<<<")||n.includes(`
|
|
217
|
+
### `)}function bl(e,t){let n=Xe(e),o=n.taskEnv??"OMNISH_TASK";return!n.promptTemplate&&jb(n,o)&&(n=Bb(n,t)),n}function Gb(e){try{let t=gl.readFileSync(Hr,"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(!(!yl.test(i)||wl.has(i))&&s&&typeof s=="object"&&typeof s.command=="string"){let l=bl(s,e),c=_n(l);if(!c.ok){console.warn(`[omnish] recipes.json: skipping "${i}": ${c.error}`);continue}o[i]=l}}return o}catch{return{}}}function zb(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 _s(e,t,n){for(let[o,r]of Object.entries(t)){let s=o.toLowerCase();!yl.test(s)||wl.has(s)||r.command.trim()&&e.set(s,{...r,name:s,source:n})}}function hl(e,t){let n=Yp(e),o={};for(let[r,s]of Object.entries(n)){let i=bl(s,t);_n(i).ok&&(o[r]=i)}return o}function Qp(e,t){let n=new Map;return _s(n,zb(t),"builtin"),_s(n,Gb(t),"global"),_s(n,hl(ko,t),"shared"),_s(n,hl(e,t),"peer"),n}function Ze(e,t,n){let o=n.trim().toLowerCase();return Qp(e,t).get(o)}function Vp(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 kl(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 vl(e){let t=kl(e);return{scope:t.scope,remainder:t.remainder}}function Xp(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 Jb(e){let t=e.trim().toLowerCase();if(t==="--global"||t==="-g")return"global";if(t==="--chat"||t==="-p")return"chat"}function Zp(e){let t=e.trim().match(/^(\S+)\s+(--global|-g|--chat|-p)\s*$/i);if(!t?.[1]||!t[2])return;let n=Jb(t[2]);if(n)return{name:t[1],target:n}}function Jt(e,t,n,o){let r=o.trim().toLowerCase(),s=e==="global"?ko:t,i=Yp(s)[r];if(i===void 0)return;let a=bl(i,n);if(_n(a).ok)return{...a,name:r,source:e==="global"?"shared":"peer"}}function em(e,t,n){if(n==="merged")return[];let o=n==="global"?ko:e,r=n==="global"?"shared":"peer",s=hl(o,t);return Object.entries(s).map(([i,a])=>({...a,name:i,source:r})).sort((i,a)=>i.name.localeCompare(a.name))}function Et(e){let t=e.trim();if(!t)return{ok:!1,error:"Name is empty."};let n=t.toLowerCase();return yl.test(n)?wl.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 vo(e,t){let n=e.replace(/\r\n/g,`
|
|
218
|
+
`).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 So(e,t){return e.includes("$")?e.includes(t):!1}function Fn(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(!So(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 _n(e){let t=e.taskEnv??"OMNISH_TASK",n=Fn(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 qb(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),p=/^(template|tamplate)\b/i.exec(d);if(!p)continue;let f=l+2+p[0].length,h=t[f]??"";if(!h||!/\s/.test(h))continue;r=l;let g=f;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,`
|
|
219
|
+
`).trim();if(!n)throw new Error("Recipe body is empty.");let o=n.match(/^--steps\s+([\s\S]+)$/i);if(o){let p=o[1].trim().split(/\s*;\s*|\n/).filter(h=>h.trim());if(p.length===0)throw new Error("No steps found. Separate steps with ; or newlines.");let f=p.map(h=>{let g=h.startsWith("+"),y=g?h.slice(1).trim():h.trim();if(!y)throw new Error("Empty step in recipe.");return{cmd:y,...g?{continueOnFail:!0}:{}}});return Xe({command:f[0].cmd,steps:f})}let r="OMNISH_TASK",{command:s,template:i}=qb(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=Fn(s,r);if(!d.ok)throw new Error(d.error);return Xe({command:s,promptTemplate:i})}let a=/\n---\n/,l=n.search(a);if(l>=0){let d=n.slice(0,l).trim(),p=n.slice(l).replace(/^\n---\n/,"").trim();if(!d)throw new Error("Command part (before ---) is empty.");if(!p)throw new Error("Template part (after ---) is empty.");let f=Fn(d,r);if(!f.ok)throw new Error(f.error);return Xe({command:d,promptTemplate:p})}if(Fn(n,r).ok)return Xe({command:n});let c=t.trim(),u=Fn(c,r);if(!u.ok)throw new Error(`recipesMacroDefaultCommand invalid (${u.error}). Fix config or paste runner then --- then template.`);return Xe({command:c,promptTemplate:n})}function Us(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 xo(e,t,n,o="chat",r){let s=Et(t);if(!s.ok)throw new Error(s.error);let i=Xe({...n,command:n.command});if(r?.skipCommandValidation){if(!i.command.trim())throw new Error("Command is empty.")}else{let c=_n(i);if(!c.ok)throw new Error(c.error)}let a=o==="global"?ko:e,l=fl(a);l[s.normalized]=i,Ws()}function tm(e,t,n="chat"){let o=t.trim().toLowerCase(),r=n==="global"?ko:e;Ds();let s=bo.get(r);return!s||!(o in s)?!1:(delete s[o],Ws(),!0)}function xl(e,t,n,o){let r=Et(t);if(!r.ok)return{ok:!1,error:r.error};let s=r.normalized,i=e,a=ko;Ds();let l=fl(i),c=fl(a),u=l[s],d=c[s],p=f=>{let h=Xe({...f}),g=_n(h);if(!g.ok)throw new Error(g.error);return h};try{if(n==="global"){if(u!==void 0){let g=p(u);return c[s]=g,delete l[s],Ws(),{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 h=Ze(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. 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 h=p(d);return l[s]=h,delete c[s],Ws(),{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 f=Ze(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.`}:{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(f){return{ok:!1,error:String(f)}}}function nm(e,t){let n=[...Qp(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 Hs(e){let t=Wb.randomBytes(4).toString("hex");return`r-${e.replace(/[^a-zA-Z0-9_-]/g,"").slice(0,12)}-${t}`.slice(0,32)}function om(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}
|
|
220
|
+
${c}`}function rm(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(`
|
|
221
|
+
`)}j();var sm={enter:"\r",cr:"\r",lf:`
|
|
222
|
+
`,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 Kb(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+=`
|
|
223
|
+
`,n++;continue}if(r==="t"){t+=" ",n++;continue}if(r==="\\"){t+="\\",n++;continue}}t+=o}return t}function Yb(e){let t=e.trim();if(!t)return"";if(t.startsWith("\\"))return Kb(t);let n=t.toLowerCase();if(sm[n])return sm[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 Bs(e){let t=e.split(",").map(n=>n.trim()).filter(Boolean);return t.length===0?"":t.map(n=>Yb(n)).join("")}var hr="\x1B";function js(e){let t=e;return t=t.replace(new RegExp(`${hr}\\[[\\d;?]*[ -/]*[@-~]`,"g"),""),t=t.replace(new RegExp(`${hr}\\][^${hr}\\u0007]*(?:\\u0007|${hr}\\\\)`,"g"),""),t=t.replace(new RegExp(`${hr}[@-Z\\\\-_]`,"g"),""),t=t.replace(/\u0007/g,""),t}function Qb(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 Vb(e,t){return`${e}${t}`}var Gs=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=Vb(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(f=>setTimeout(f,a)),this.closed)return;let l=this.pending.get(t)??"";if(this.pending.delete(t),!l||(this.opts.isRaw(n,o)||(l=js(l)),!l.trim()))return;let c=r.appsMaxFlushBytes;if(l.length>c){let f=l.slice(c);l=l.slice(0,c)+`
|
|
224
|
+
[\u2026+${f.length} chars; /apps since ${o} to read more]`,this.pending.set(t,(this.pending.get(t)??"")+f)}let d=(this.opts.getRunningCount(n)>1?`[${o}] `:"")+l,p=Qb(d,r.appsMaxWaChars);for(let f of p){if(this.closed)break;try{await this.opts.send(n,f)}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 zs=4096,Xb=/\[sudo\]\s+password\s+for\s+/i,Zb=/passphrase\s+for\s+/i,ek=/(?:^|\n)\s*(?:Password|password):\s*$/;function Js(e){let n=js(e).replace(/\r/g,"").slice(-512);return!!(Xb.test(n)||Zb.test(n)||ek.test(n))}j();import tk from"node:fs";import nk from"node:path";import*as im from"node-pty";var ok=1024*1024,qs=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>ok&&this.ringChunks.length>0;){let n=this.ringChunks.shift();this.ringBytes-=n.length}}static start(t){U(Ko(t.peerKey));let n=nk.join(Ko(t.peerKey),`${t.name}.log`),o=tk.createWriteStream(n,{flags:"a",mode:384}),r={...process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...t.cwd?{PWD:t.cwd}:{},...t.extraEnv??{}},s=im.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 Cl(e){return new Promise(t=>setTimeout(t,e))}async function Ks(e,t,n){let o=n.appsSubmitDelayMs,r=n.appsClearInputDelayMs,s=n.appsClearInput!==!1,a=n.appsSkipClearOnPasswordPrompt!==!1&&Js(e.recentOutputTail(zs)),l=s&&!a?Bs(n.appsClearInputSequence||"^A,^K"):"",u=t.replace(/\r\n/g,`
|
|
216
225
|
`).replace(/\r/g,"").split(`
|
|
217
|
-
`);l&&(r>0&&await
|
|
218
|
-
`).trimEnd()}catch{return""}}var
|
|
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=
|
|
226
|
+
`);l&&(r>0&&await Cl(r),e.write(l));for(let d of u)d.length>0&&e.write(d),o>0&&await Cl(o),e.write("\r"),l&&(r>0&&await Cl(r),e.write(l))}var sk=/^[a-zA-Z0-9_-]{1,32}$/;function qe(e){return sk.test(e)?null:"Session name must be 1\u201332 chars: letters, digits, _ or -."}function _e(e,t){return`${e}:${t}`}function ik(e){let t=e.lastIndexOf(":");return t<=0?null:{peerKey:e.slice(0,t),name:e.slice(t+1)}}function ak(e){return/\bstarted\b/i.test(e)&&!/^Session "/.test(e)&&!/^Per-chat app/.test(e)&&!/^Global app/.test(e)}function Rl(e,t){let n=e??t.recipesRunAttach;return{attach:n,mute:!n}}function lk(e,t){return e===0&&(t===0||t==null)}function ck(e,t){try{let o=hn.readFileSync(e,"utf8").split(/\r?\n/);return o.slice(Math.max(0,o.length-t)).join(`
|
|
227
|
+
`).trimEnd()}catch{return""}}var gn=class{constructor(t,n){this.getCfg=t;this.send=n,this.router=new Gs({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 rk.join(Ko(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(zs);if(!Js(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 Ks(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 Ks(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,p=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}). ${p}`}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(`
|
|
228
|
+
`)}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=Hs(r.recipeLabel),i=this.runQueue.get(t)?.length??0,a=r.startOptions??Rl(null,n),l=this.start(t,s,r.command,n,r.extraEnv,a);if(!ak(l))return o.unshift(r),this.runQueue.set(t,o),this.runQueuePaused.set(t,!0),this.activeQueuedRunHead.set(t,null),`${l}
|
|
220
229
|
Run queue paused; fix the issue, then /run queue resume.`;this.activeQueuedRunHead.set(t,{sessionName:s,recipeLabel:r.recipeLabel});let u=i>0?`
|
|
221
230
|
Run queue: started head above; ${i} job(s) still waiting in FIFO.`:`
|
|
222
231
|
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.`;
|
|
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
|
|
232
|
+
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.`;re();let l=se(t).cwd,c={...process.env,TERM:"xterm-256color",COLORTERM:"truecolor",...l?{PWD:l}:{},...s??{}},u;try{u=qs.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)}
|
|
233
|
+
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 p=i?.attach!==!1,f=i?.mute??!p;if(p&&this.focus.set(t,n),f&&this.muted.add(d),p)return`App "${n}" started and attached.
|
|
225
234
|
[cwd: ${l}]
|
|
226
|
-
Plain DMs go to this session until /apps detach. Use >othername text for another session.`;let f
|
|
235
|
+
Plain DMs go to this session until /apps detach. Use >othername text for another session.`;let h=f?`
|
|
227
236
|
Output muted \u2014 /apps unmute `+n+" or /apps attach "+n+" to stream to chat.":"";return`App "${n}" started (detached).
|
|
228
|
-
[cwd: ${l}]`+
|
|
237
|
+
[cwd: ${l}]`+h+`
|
|
229
238
|
/apps attach ${n} \u2014 focus for plain DMs
|
|
230
239
|
>${n} <text> \u2014 send a line
|
|
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=
|
|
240
|
+
/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=lk(o.exitCode,o.signal),p=this.runQueue.get(t)?.length??0;if(d){if(this.runQueuePaused.set(t,!1),p>0){let f=this.drainNextQueuedRun(t,u);if(f)try{await this.send(t,f)}catch{}}return}if(this.runQueuePaused.set(t,!0),p>0){let f=`Run queue paused (exit code=${o.exitCode}${o.signal!=null?` signal=${o.signal}`:""}). ${p} run(s) waiting. /run queue resume to continue.`;try{await this.send(t,f)}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=ik(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)"}
|
|
232
241
|
App sessions:
|
|
233
242
|
${n.join(`
|
|
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=
|
|
243
|
+
`)}`}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=hn.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
244
|
`)}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
|
|
237
|
-
[...truncated]`}function
|
|
245
|
+
`);return await Ks(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=Bs(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(!hn.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 ck(s,a)||"(empty log)"}readSince(t,n,o){let r=this.logPath(t,n);try{let i=hn.statSync(r).size,a=hn.openSync(r,"r");try{let l=Math.min(o,i),c=i-l,u=Buffer.alloc(c);return c>0&&hn.readSync(a,u,0,c,l),{text:u.toString("utf8"),nextOffset:i}}finally{hn.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{hn.rmSync(s,{force:!0})}catch{}return`Removed "${n}" metadata and log.`}};ot();xe();import yr from"node:fs";import wr from"node:path";import uk from"node:os";import*as lm from"node-pty";function dk(e,t){return e.length<=t?e:`${e.slice(0,t)}
|
|
246
|
+
[...truncated]`}function pk(e){if(e===void 0||e===0)return null;let t=uk.constants.signals;for(let[n,o]of Object.entries(t))if(o===e)return n;return null}function am(e){try{process.platform==="win32"?e.kill():e.kill("SIGTERM")}catch{e.kill()}}function Wn(e,t,n){return new Promise(o=>{let r=Date.now(),s=!1,i="",a=n.cwd,l=null,c=!1,u=null,d=null,p,f=y=>{if(c)return;c=!0,p!==void 0&&clearTimeout(p),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=lm.spawn(e,["-c",t],{name:"xterm-256color",cols:120,rows:40,cwd:a,env:g})}catch(y){f({code:null,stdout:"",stderr:String(y),durationMs:Date.now()-r,timedOut:!1,signal:null});return}p=setTimeout(()=>{s=!0,l&&am(l)},n.timeoutMs),u=l.onData(y=>{i+=y,i.length>n.maxBytes&&(i=dk(i,n.maxBytes),l&&am(l))}),d=l.onExit(y=>{f({code:y.exitCode,stdout:i,stderr:"",durationMs:Date.now()-r,timedOut:s,signal:pk(y.signal)})})})}function gr(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 mk(e,t,n){let o=e;for(let r=0;r<14;r++){let s=gr(o,t,n),i=new Date(s).getDay();if(i>=1&&i<=5)return s;o=s}return gr(o,t,n)}function fk(e,t,n,o){let r=e;for(let s=0;s<370;s++){let i=gr(r,n,o);if(new Date(i).getDay()===t)return i;r=i}return gr(r,n,o)}function hk(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 gk(e,t){return e.kind==="ondemand"||e.kind==="heartbeat"?Number.POSITIVE_INFINITY:e.kind==="daily"?gr(t,e.hour,e.minute):e.kind==="weekdays"?mk(t,e.hour,e.minute):e.kind==="hourly"?hk(t,e.minute):fk(t,e.weekday,e.hour,e.minute)}function dm(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=gk(e,a);if(l>o)break;i.push(l),a=l}return i}var yk={sun:0,sunday:0,mon:1,monday:1,tue:2,tues:2,tuesday:2,wed:3,wednesday:3,thu:4,thur:4,thurs:4,thursday:4,fri:5,friday:5,sat:6,saturday:6};function Tl(e){let t=/^(\d{1,2}):(\d{2})$/.exec(e.trim());if(!t)return null;let n=Number(t[1]),o=Number(t[2]);return!Number.isFinite(n)||!Number.isFinite(o)||n>23||o>59?null:{hour:n,minute:o}}function wk(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 Ys(e){if(e.length===0)return{ok:!1,error:"Missing schedule (try: ondemand, hourly [:MM], daily HH:MM, weekdays HH:MM, weekly <dow> HH:MM)."};let t=e[0].toLowerCase();if(t==="ondemand"||t==="manual")return e.length!==1?{ok:!1,error:"ondemand takes no extra tokens."}:{ok:!0,schedule:{kind:"ondemand"}};if(t==="daily"){if(e.length!==2)return{ok:!1,error:"Usage: daily HH:MM"};let n=Tl(e[1]);return n?{ok:!0,schedule:{kind:"daily",hour:n.hour,minute:n.minute}}:{ok:!1,error:"daily needs HH:MM (24h)."}}if(t==="weekdays"){if(e.length!==2)return{ok:!1,error:"Usage: weekdays HH:MM"};let n=Tl(e[1]);return n?{ok:!0,schedule:{kind:"weekdays",hour:n.hour,minute:n.minute}}:{ok:!1,error:"weekdays needs HH:MM (24h)."}}if(t==="hourly"){if(e.length===1)return{ok:!0,schedule:{kind:"hourly",minute:0}};if(e.length!==2)return{ok:!1,error:"Usage: hourly | hourly :MM | hourly MM (minute 0\u201359)."};let n=wk(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=yk[n];if(o===void 0){let s=Number(n);Number.isInteger(s)&&s>=0&&s<=6&&(o=s)}if(o===void 0)return{ok:!1,error:`Unknown weekday "${e[1]}". Use mon\u2026sun or 0\u20136 (Sun=0).`};let r=Tl(e[2]);return r?{ok:!0,schedule:{kind:"weekly",weekday:o,hour:r.hour,minute:r.minute}}:{ok:!1,error:"weekly needs HH:MM (24h) after weekday."}}if(t==="heartbeat"){if(e.length<2||e.length>3)return{ok:!1,error:"Usage: heartbeat <interval> [grace] \u2014 e.g. heartbeat 1h, heartbeat 30m 10m"};let n=cm(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=cm(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 cm(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 um(e){let t=Math.floor(e/1e3),n=Math.floor(t/86400),o=Math.floor(t%86400/3600),r=Math.floor(t%3600/60),s=[];return n>0&&s.push(`${n}d`),o>0&&s.push(`${o}h`),r>0&&s.push(`${r}m`),s.length>0?s.join(""):`${t}s`}function Co(e){let t=o=>o<10?`0${o}`:String(o),n=(o,r)=>`${t(o)}:${t(r)}`;switch(e.kind){case"ondemand":return"ondemand";case"daily":return`daily ${n(e.hour,e.minute)}`;case"weekdays":return`weekdays ${n(e.hour,e.minute)}`;case"hourly":return e.minute===0?"hourly":`hourly :${t(e.minute)}`;case"weekly":return`weekly ${["sun","mon","tue","wed","thu","fri","sat"][e.weekday]??e.weekday} ${n(e.hour,e.minute)}`;case"heartbeat":return`heartbeat every ${um(e.intervalMs)} grace ${um(e.graceMs)}`}}j();xe();import bk from"better-sqlite3";var pm=2,yn=null;function mm(){U(St);let e=new bk(qc);e.pragma("journal_mode = WAL"),e.exec(`
|
|
238
247
|
CREATE TABLE IF NOT EXISTS cowork_meta (
|
|
239
248
|
key TEXT PRIMARY KEY,
|
|
240
249
|
value TEXT NOT NULL
|
|
@@ -253,199 +262,200 @@ ${n.join(`
|
|
|
253
262
|
last_ok INTEGER NOT NULL,
|
|
254
263
|
updated_at_ms INTEGER NOT NULL
|
|
255
264
|
);
|
|
256
|
-
`);let t=e.prepare("SELECT value FROM cowork_meta WHERE key = 'schema_version'").get(),n=t?Number(t.value):0;return n<
|
|
265
|
+
`);let t=e.prepare("SELECT value FROM cowork_meta WHERE key = 'schema_version'").get(),n=t?Number(t.value):0;return n<pm&&(n<2&&e.exec(`
|
|
257
266
|
CREATE TABLE IF NOT EXISTS cowork_task_state (
|
|
258
267
|
task_id TEXT PRIMARY KEY,
|
|
259
268
|
last_ok INTEGER NOT NULL,
|
|
260
269
|
updated_at_ms INTEGER NOT NULL
|
|
261
270
|
);
|
|
262
|
-
`),e.prepare("INSERT OR REPLACE INTO cowork_meta (key, value) VALUES ('schema_version', ?)").run(String(
|
|
271
|
+
`),e.prepare("INSERT OR REPLACE INTO cowork_meta (key, value) VALUES ('schema_version', ?)").run(String(pm))),e}function Dn(e){yn||(yn=mm()),vk(e)}function fm(){if(yn){try{yn.close()}catch{}yn=null}}function Un(){return yn||(yn=mm()),yn}function kk(e){let n=Un().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 Qs(e){let t=kk(e.id);return t??e.lastCompletedSlotMs}function Vs(e,t){if(t.length===0)return;let n=Un(),o=n.prepare(`
|
|
263
272
|
INSERT INTO cowork_slot_completion (task_id, slot_ms, kind, completed_at_ms, log_path)
|
|
264
273
|
VALUES (?, ?, ?, ?, ?)
|
|
265
|
-
`);n.transaction(()=>{for(let s of t)o.run(e,s.slotMs,s.kind,s.completedAtMs,s.logPath)})()}function
|
|
274
|
+
`);n.transaction(()=>{for(let s of t)o.run(e,s.slotMs,s.kind,s.completedAtMs,s.logPath)})()}function $l(e){Un().prepare("INSERT OR REPLACE INTO cowork_task_state (task_id, last_ok, updated_at_ms) VALUES (?, 1, ?)").run(e,Date.now())}function Xs(e){return Un().prepare("SELECT updated_at_ms FROM cowork_task_state WHERE task_id = ?").get(e)?.updated_at_ms??null}function Zs(e){let t=Un().prepare("SELECT last_ok FROM cowork_task_state WHERE task_id = ?").get(e);return t==null?null:t.last_ok===1}function ei(e,t){Un().prepare("INSERT OR REPLACE INTO cowork_task_state (task_id, last_ok, updated_at_ms) VALUES (?, ?, ?)").run(e,t?1:0,Date.now())}function vk(e){let n=Un().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{Vs(r.id,[{slotMs:r.lastCompletedSlotMs,kind:"migrated",logPath:null,completedAtMs:o}]),M.info({taskId:r.id,slotMs:r.lastCompletedSlotMs},"cowork seeded completion from tasks.json")}catch(i){M.warn({err:String(i),taskId:r.id},"cowork seed from tasks.json failed")}}function Ml(e,t){if(e.startsWith("tg:")){let n=e.slice(3);return Jr(t.telegramAllowFrom).has(n)}return Zc(zr(t.allowFrom),e.replace(/^wa:/,""))}var Sk=8,hm=12e4,gm=10,xk=1e3;function Ck(e){return e.toISOString().replace(/[:.]/g,"-")}function Rk(e){let t="";for(let n of e)n==="*"?t+="[^/\\\\]*":n==="?"?t+="[^/\\\\]":/[.+^${}()|[\]\\]/.test(n)?t+=`\\${n}`:t+=n;return new RegExp(`^${t}$`)}function Tk(e,t,n){let o=et(e,t),r=wr.dirname(o),s=wr.basename(o);if(r.includes("*")||r.includes("?"))return[];if(!s.includes("*")&&!s.includes("?")){try{let c=yr.statSync(o);if(c.isFile()&&c.mtimeMs>=n)return[o]}catch{}return[]}let i;try{i=yr.readdirSync(r)}catch{return[]}let a=Rk(s),l=[];for(let c of i){if(!a.test(c))continue;let u=wr.join(r,c);try{let d=yr.statSync(u);d.isFile()&&d.mtimeMs>=n&&l.push(u)}catch{}}return l}function ym(e,t,n){let o=ht(e,t.fileSendMaxBytes);return"error"in o?{ok:!1,displayName:n??wr.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 wm(e,t,n,o){let r=se(t.ownerPeerKey),s=t.cwd.trim()?t.cwd:r.cwd,i=et(t.outputDir,r.cwd);try{yr.mkdirSync(i,{recursive:!0,mode:448})}catch(P){M.warn({err:String(P),outDir:i},"cowork mkdir outputDir")}let a=n.slotMs!==null?new Date(n.slotMs).toLocaleString(void 0,{dateStyle:"short",timeStyle:"short"}):"on-demand",l=n.onDemand?"on-demand":n.catchUp?"catch-up":"scheduled",c=Date.now(),u=await Wn(e.shell,t.command,{timeoutMs:e.syncTimeoutMs,maxBytes:e.syncMaxBytes,cwd:s}),d=Date.now()-c,p=`${Ck(new Date)}-${t.id}-${l}.log`,f=wr.join(i,p),g=[`cowork task=${t.name} id=${t.id}`,`slot=${a} kind=${l}`,`cwd=${s}`,`exit=${u.code} timedOut=${u.timedOut} durationMs=${d}`,"---",""].join(`
|
|
266
275
|
`)+(u.stdout||"")+(u.stderr?`
|
|
267
276
|
--- stderr ---
|
|
268
|
-
${u.stderr}`:"");try{
|
|
269
|
-
`),$e=
|
|
270
|
-
[...input truncated]`}function
|
|
277
|
+
${u.stderr}`:"");try{yr.writeFileSync(f,g,{mode:384})}catch(P){M.warn({err:String(P),logPath:f},"cowork write log")}let y=u.code===0&&!u.timedOut&&u.signal===null,k=Pe().find(P=>P.id===t.id&&P.ownerPeerKey===t.ownerPeerKey),x=k?.notify??t.notify,T=k?.notifyWhen??t.notifyWhen??"always",O=k?.attachLog??t.attachLog,C=k?.attachFiles??t.attachFiles,N=!0;if(T==="failure")N=!y;else if(T==="state-change"){let P=Zs(t.id);N=P===null||P!==y}ei(t.id,y);let I=u.timedOut?"timeout":u.signal?`signal ${u.signal}`:u.code!==0&&u.code!==null?`exit ${u.code}`:null,K=`slot: ${a} \xB7 ${l}${I?` \xB7 ${I}`:""}`,ee=(u.stdout||"").replace(/\s+$/,""),le=(u.stderr||"").trim(),Se=ee||(le?`(stderr) ${le}`:"(no output)"),_=T==="state-change"?y?" [recovered]":" [failing]":"",ge=`${t.name}${_} : ${I?`[${I}] `:""}${Se}`,V=n.onDemand?[K,`output: ${Se}`]:[ge];if(N){let P=[],G=[],Y=c-xk,z=[],ie=new Set;if(Array.isArray(C))for(let $ of C){let L;try{L=Tk($,s,Y)}catch(te){M.warn({err:String(te),pat:$},"cowork attach glob"),P.push(`attach: ${$} skipped (glob error)`);continue}if(L.length!==0)for(let te of L)ie.has(te)||(ie.add(te),z.push(te))}if(O){let $=ym(f,e,`${t.name}-${l}.log`);$.ok?G.push($.spec):P.push(`attach: ${$.displayName} skipped (${$.reason})`)}let J=0;for(let $ of z){if(G.length>=gm){J+=1;continue}let L=ym($,e);L.ok?G.push(L.spec):P.push(`attach: ${L.displayName} skipped (${L.reason})`)}J>0&&P.push(`attached: skipped ${J} file(s) over cap ${gm}`),P.length>0&&V.push(...P);let Be=V.join(`
|
|
278
|
+
`),$e=m(Be),Ne=ro(x,t.ownerPeerKey,e);for(let $ of Ne){let L=ue($e,$.startsWith("tg:")?"telegram":"whatsapp").text;try{await o.sendToPeer($,L)}catch(te){M.warn({err:String(te),pk:$},"cowork notify failed")}for(let te of G)try{await o.sendMediaToPeer($,te)}catch(be){M.warn({err:String(be),pk:$,file:te.displayName},"cowork media notify failed")}}}return{commandOk:y,logPath:f}}function ti(e){let t=!1;Dn(Pe());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:p,remainingAfter:f}=su(Sk);a=p.length,l=f,i=p.length+f;for(let y of p)try{let k=Pe().find(x=>x.name===y.name.toLowerCase()&&x.ownerPeerKey===y.ownerPeerKey);if(k&&k.enabled){if(!Ml(k.ownerPeerKey,d)){M.warn({task:k.name,peer:k.ownerPeerKey},"cowork: skipping on-demand run \u2014 owner no longer on allowlist");continue}c+=1,await wm(d,k,{slotMs:null,catchUp:!1,onDemand:!0},e)}}catch(b){M.warn({err:String(b),pending:y.name},"cowork on-demand run failed")}let h=Pe();Dn(h);let g=Date.now();for(let y of h){if(!y.enabled||y.schedule.kind!=="heartbeat"||!Ml(y.ownerPeerKey,d))continue;let b=Xs(y.id);if(b===null)continue;let k=b+y.schedule.intervalMs+y.schedule.graceMs;if(g>k){if(Zs(y.id)===!1)continue;ei(y.id,!1);let T=Math.round((g-b)/6e4),O=`${y.name} [heartbeat missed] \u2014 last check-in ${T}m ago`,C=y.notify,N=ro(C,y.ownerPeerKey,d);for(let I of N)try{await e.sendToPeer(I,O)}catch(K){M.warn({err:String(K),pk:I},"cowork heartbeat notify failed")}}else if(g<=k&&Zs(y.id)===!1){ei(y.id,!0);let T=`${y.name} [heartbeat recovered]`,O=y.notify,C=ro(O,y.ownerPeerKey,d);for(let N of C)try{await e.sendToPeer(N,T)}catch(I){M.warn({err:String(I),pk:N},"cowork heartbeat recovery notify failed")}}}for(let y of h){if(!y.enabled||y.schedule.kind==="ondemand"||y.schedule.kind==="heartbeat")continue;if(!Ml(y.ownerPeerKey,d)){M.warn({task:y.name,peer:y.ownerPeerKey},"cowork: skipping scheduled run \u2014 owner no longer on allowlist");continue}let b=Qs(y),k=dm(y.schedule,b,y.createdAtMs,g);if(k.length===0)continue;let x=k[k.length-1],T=g-x>hm;try{u+=1;let{commandOk:O,logPath:C}=await wm(d,y,{slotMs:x,catchUp:T,onDemand:!1},e);if(O){let N=Date.now(),I=g-x<=hm?"on_time":"catch_up";if(k.length===1)Vs(y.id,[{slotMs:x,kind:I,logPath:C,completedAtMs:N}]);else{let ee=k.slice(0,-1).map(le=>({slotMs:le,kind:"coalesced",logPath:null,completedAtMs:N}));ee.push({slotMs:x,kind:I,logPath:C,completedAtMs:N}),Vs(y.id,ee)}}}catch(O){M.warn({err:String(O),task:y.name},"cowork scheduled run failed")}}}finally{let d=Date.now()-s;M.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),fm()}}import $k from"node:fs";import bm from"node:path";import{fileURLToPath as Mk}from"node:url";var ni=null;function lt(){if(ni!==null)return ni;let e=bm.dirname(Mk(import.meta.url)),t=bm.join(e,"..","package.json"),n=$k.readFileSync(t,"utf8"),o=JSON.parse(n);return ni=typeof o.version=="string"&&o.version.trim()?o.version.trim():"0.0.0",ni}xe();j();import{spawn as Pk}from"node:child_process";import km from"node:fs";import vm from"node:path";var Ek=new Set(["PATH","HOME","USER","LOGNAME","SHELL","LANG","LC_ALL","LC_CTYPE","LC_MESSAGES","TMPDIR","TZ"]),Ak=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 Ik(e,t){let n={OMNISH_PEER_KEY:e,OMNISH_CHAT_MESSAGE:t};for(let o of Ek){let r=process.env[o];r!==void 0&&(n[o]=r)}for(let[o,r]of Object.entries(process.env))r&&(o.startsWith("OMNISH_")||Ak.has(o))&&(n[o]=r);return n}var Sm=new Map;function Lk(e,t){let o=(Sm.get(e)??Promise.resolve()).then(t).catch(r=>{M.warn({peerKey:e,err:String(r)},"chat LLM fallback queue task failed")});Sm.set(e,o)}function Ok(e){re();let t=e.chatLlmWorkDir.trim();if(t){let o=vm.resolve(t);return U(o),{cwd:o,cleanup:()=>{}}}let n=km.mkdtempSync(vm.join(D,"chat-llm-"));return{cwd:n,cleanup:()=>{try{km.rmSync(n,{recursive:!0,force:!0})}catch{}}}}function Nk(e,t){return e.length<=t?e:`${e.slice(0,t)}
|
|
279
|
+
[...input truncated]`}function Fk(e,t){let n=[],o=e.stdout.trimEnd(),r=e.stderr.trimEnd();return o&&n.push(o.length>t?`${o.slice(0,t)}
|
|
271
280
|
[...truncated]`:o),r&&(n.push("\u2014 stderr \u2014"),n.push(r.length>t?`${r.slice(0,t)}
|
|
272
281
|
[...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(`
|
|
273
|
-
`)}function
|
|
274
|
-
[...truncated]`;else{let
|
|
275
|
-
[...truncated]`}try{u?.kill("SIGTERM")}catch{}}};try{u
|
|
276
|
-
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),(()=>{if(!(!g||c))try{g.write(n,"utf8",b=>{if(b&&!
|
|
277
|
-
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}
|
|
278
|
-
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}
|
|
279
|
-
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),u.on("close",(b,k)=>{
|
|
280
|
-
`,{mode:384})}function
|
|
281
|
-
`)}var
|
|
282
|
-
[spawn error] ${String(
|
|
283
|
-
`)}catch{}});let
|
|
282
|
+
`)}function oi(e){let t=e&&typeof e=="object"&&"code"in e?String(e.code):"";return t==="EPIPE"||t==="EOF"}function xm(e){try{e?.stdin?.end()}catch(t){if(!oi(t))throw t}}function _k(e,t,n,o){return new Promise(r=>{let s=Date.now(),i=!1,a="",l="",c=!1,u=null,d,p=b=>{c||(c=!0,clearTimeout(d),r(b))},f=o.maxOutChars,h=(b,k)=>{b==="out"?a+=k:l+=k;let x=a.length+l.length;if(x>f){let T=x-f;if(l.length>=T)l=`${l.slice(0,Math.max(0,l.length-T))}
|
|
283
|
+
[...truncated]`;else{let O=T-l.length;l="",a=`${a.slice(0,Math.max(0,a.length-O))}
|
|
284
|
+
[...truncated]`}try{u?.kill("SIGTERM")}catch{}}};try{u=Pk(e,["-c",t],{cwd:o.cwd,env:o.env,stdio:["pipe","pipe","pipe"]})}catch(b){p({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=>h("out",b)),u.stderr?.on("data",b=>h("err",b));let g=u.stdin;g&&g.on("error",b=>{oi(b)||p({code:null,stdout:a,stderr:`${l}
|
|
285
|
+
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),(()=>{if(!(!g||c))try{g.write(n,"utf8",b=>{if(b&&!oi(b)){try{u?.kill("SIGTERM")}catch{}p({code:null,stdout:a,stderr:`${l}
|
|
286
|
+
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}xm(u)})}catch(b){if(!oi(b)){p({code:null,stdout:a,stderr:`${l}
|
|
287
|
+
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null});return}xm(u)}})(),u.on("error",b=>{p({code:null,stdout:a,stderr:`${l}
|
|
288
|
+
${String(b)}`,durationMs:Date.now()-s,timedOut:i,signal:null})}),u.on("close",(b,k)=>{p({code:b,stdout:a,stderr:l,durationMs:Date.now()-s,timedOut:i,signal:k??null})})})}async function Wk(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}=Ok(e);try{let a=Nk(n,e.chatLlmMaxInputChars),l=Ik(t,a),c=e.chatLlmMaxOutputChars,u;e.chatLlmNeedsTty?u=await Wn(e.shell,r,{cwd:s,timeoutMs:e.chatLlmTimeoutMs,maxBytes:c,env:l}):u=await _k(e.shell,r,a,{cwd:s,timeoutMs:e.chatLlmTimeoutMs,maxOutChars:c,env:l});let d=Fk(u,c);await o(d)}catch(a){M.warn({peerKey:t,err:String(a)},"chat LLM fallback run failed"),await o(`(assistant error) ${String(a)}`)}finally{i()}}function Ro(e,t,n,o){M.info({peerKey:t,len:n.length},"chat LLM fallback enqueued"),Lk(t,async()=>{await Wk(e,t,n,o),M.info({peerKey:t},"chat LLM fallback completed")})}j();import{spawn as Dk}from"node:child_process";import Uk from"node:crypto";import nt from"node:fs";import Pl from"node:path";var Cm=64,Hk=/^[a-zA-Z0-9._-]+$/;function Bk(e){let t=e.trim();return t?t.length>Cm?`Job name must be at most ${Cm} characters.`:Hk.test(t)?null:"Job name may only use letters, digits, and . _ -":"Job name must not be empty."}function Rm(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=Bk(n);if(i)return{error:i}}return s?{cmd:s,name:n,notify:o}:{error:"Add a shell command after the flags."}}function jk(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 br(e,t){nt.writeFileSync(e,JSON.stringify(t,null,2)+`
|
|
289
|
+
`,{mode:384})}function kr(e){try{return JSON.parse(nt.readFileSync(e,"utf8"))}catch{return null}}function ri(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(`
|
|
290
|
+
`)}var qt=class{running=new Map;onJobExit;constructor(t={}){this.onJobExit=t.onJobExit}finishJob(t,n,o,r){br(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={}){re();let r=Uk.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=Dk(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 br(i,d),u.stdout?.on("data",p=>{a.write(p)}),u.stderr?.on("data",p=>{a.write(p)}),u.on("close",(p,f)=>{this.running.delete(r),a.end();let h=kr(i)??d,g={...h,status:h.status==="killed"?"killed":"done",exitCode:p,signal:f??null,finishedAt:new Date().toISOString()};this.finishJob(i,d,g,o)}),u.on("error",p=>{this.running.delete(r),a.end(()=>{try{nt.appendFileSync(s,`
|
|
291
|
+
[spawn error] ${String(p)}
|
|
292
|
+
`)}catch{}});let h={...kr(i)??d,status:"done",exitCode:null,signal:null,finishedAt:new Date().toISOString()};this.finishJob(i,d,h,o)}),{id:r,meta:d}}list(){re();let t=[];try{t=nt.readdirSync(ut)}catch{return[]}let n=[];for(let o of t){if(!o.endsWith(".meta.json"))continue;let r=kr(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(`
|
|
284
293
|
`);return s.slice(Math.max(0,s.length-n)).join(`
|
|
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
|
|
286
|
-
`).trim()}function
|
|
294
|
+
`).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 jk(this.list(),t)}kill(t){let n=this.metaPath(t),o=kr(n);if(!o)return`Unknown job id: ${t}`;let r=this.running.get(t);if(r&&!r.killed)return r.kill("SIGTERM"),br(n,{...o,status:"killed",signal:"SIGTERM"}),`Sent SIGTERM to job ${t} (pid ${o.pid??"?"})`;if(o.pid)try{return process.kill(o.pid,"SIGTERM"),br(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=kr(this.metaPath(t));o&&br(this.metaPath(t),{...o,status:"killed",signal:"SIGTERM"})}this.running.clear()}};import xv from"node:fs";import{Bot as Cv,InputFile as Rv}from"grammy";j();import Tm from"node:fs";import To from"node:path";function Gk(e){return e.replace(/[^a-zA-Z0-9._-]+/g,"_").slice(0,120)||"unknown"}function zk(e){let o=To.basename(e.trim()||"file").replace(/[^a-zA-Z0-9._-]+/g,"_").replace(/^\.+/,"").slice(0,180);return o.length>0?o:"file"}function Jk(e,t){let n=new Date().toISOString().slice(0,10),o=Gk(t);return To.join(e,o,n)}function $o(e,t,n){let o=Jk(e,t);U(o);let r=zk(n),s=To.join(o,r);if(!Tm.existsSync(s))return s;let i=To.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=To.join(o,c),!Tm.existsSync(s))return s}return To.join(o,`${a}-${Date.now()}${i}`)}xe();import qk from"node:dns";import Kk from"node:https";import{URL as Yk}from"node:url";function Mo(e){if(e instanceof Error){let t=e.cause;return t!=null?`${e.message} (${String(t)})`:e.message}return String(e)}xe();function $m(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 Qk(e,t){let o=t.split("/").filter(r=>r.length>0).map(encodeURIComponent).join("/");return`https://api.telegram.org/file/bot${e}/${o}`}var Mm="omnish (Telegram bot; https://github.com/labKnowledge/whatsLive)";function Vk(e){let t=new Yk(e);return new Promise((n,o)=>{let r=Kk.request({hostname:t.hostname,port:t.port||443,path:t.pathname+t.search,method:"GET",headers:{"User-Agent":Mm,Accept:"*/*"},lookup(s,i,a){qk.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 Pm(e,t,n){let o;try{o=await e.api.getFile(t)}catch(i){return M.warn({err:String(i),fileId:t},"telegram getFile failed"),{error:"Could not resolve file on Telegram."}}if(!o.file_path)return{error:"Telegram did not return a file path."};let r=Qk(e.token,o.file_path),s;try{let i=await fetch(r,{headers:{"User-Agent":Mm,Accept:"*/*"}});if(!i.ok)return M.warn({status:i.status,statusText:i.statusText,fileId:t},"telegram file fetch HTTP error"),{error:`Could not download file from Telegram (HTTP ${i.status}${i.statusText?` ${i.statusText}`:""}).`};try{s=Buffer.from(await i.arrayBuffer())}catch(a){return M.warn({err:String(a),fileId:t},"telegram file read body failed"),{error:`Could not read file from Telegram (${String(a)}).`}}}catch(i){M.warn({err:String(i),fileId:t},"telegram file fetch failed; retrying via HTTPS IPv4");try{let a=await Vk(r);if(a.status<200||a.status>=300)return M.warn({status:a.status,fileId:t},"telegram HTTPS IPv4 fallback HTTP error"),{error:`Could not download file from Telegram (HTTP ${a.status}).`};s=a.buffer}catch(a){return M.warn({err:String(i),err2:String(a),fileId:t},"telegram file download failed (fetch and IPv4 fallback)"),{error:`Could not download file from Telegram (network: ${Mo(i)}; IPv4 fallback: ${Mo(a)}).`}}}return n>0&&s.byteLength>n?{error:`Media too large (max ${n} bytes).`}:{buffer:s}}ce();ot();import{downloadMediaMessage as tv,extensionForMediaMessage as nv,getContentType as si,isJidGroup as Lm,isLidUser as ov}from"@whiskeysockets/baileys";import rv from"node:fs";xe();var wn=new Map;function Xk(){let e=Date.now();for(let[t,n]of wn)e-n>6e5&&wn.delete(t);for(;wn.size>500;){let t=wn.keys().next().value;if(t===void 0)break;wn.delete(t)}}function Zk(e){!e||typeof e!="string"||(Xk(),wn.set(e,Date.now()))}function El(e){Zk(e?.key?.id??void 0)}function ev(e){if(!e)return!1;let t=wn.get(e);return t===void 0?!1:Date.now()-t>6e5?(wn.delete(e),!1):!0}function Em(e){return!ev(e)}function sv(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 iv(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 Om(e){return[sv(e),iv(e)].filter(n=>n.trim().length>0).join(`
|
|
295
|
+
`).trim()}function Al(e){return e.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,"").replace(/\uFEFF/g,"").trim()}function Nm(e){let t=si(e??void 0);return t==="imageMessage"||t==="videoMessage"||t==="audioMessage"||t==="documentMessage"||t==="stickerMessage"}function av(e){let t=si(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 Am(e){try{if(!e)return"file.bin";let t=nv(e);return`file${t&&t.length>0?t.startsWith(".")?t:`.${t}`:".bin"}`}catch{return`${(si(e??void 0)??"file").replace("Message","")||"file"}.bin`}}function lv(e){let t=si(e??void 0);if(!t)return Am(e);let n=e?.[t];return n?.fileName&&typeof n.fileName=="string"&&n.fileName.trim()?n.fileName.trim():Am(e)}async function Im(e,t,n,o){let r=t.message??void 0;if(!Nm(r))return{};let s=o.fileReceiveMaxBytes,i=av(r);if(s>0&&i!==void 0&&i>s)return{error:`Media too large (max ${s} bytes).`};let a;try{a=await tv(t,"buffer",{},{logger:e.logger,reuploadRequest:e.updateMediaMessage})}catch(d){return M.warn({err:String(d),detail:Mo(d)},"whatsapp downloadMediaMessage failed"),{error:`Could not download media from WhatsApp (${Mo(d)}).`}}if(s>0&&a.length>s)return{error:`Media too large (max ${s} bytes).`};let l;try{l=nn(o,n)}catch(d){return{error:String(d)}}let c=lv(r),u=$o(l,n,c);try{rv.writeFileSync(u,a,{mode:384})}catch{return{error:"Could not write media to inbox."}}return{path:u}}async function cv(e,t){let n=t.remoteJid??"";if(!n)return{fromJid:"",fromE164:""};if(t.remoteJidAlt){let o=oe(t.remoteJidAlt)??oe(n)??"";return{fromJid:n,fromE164:o}}if(ov(n))try{let o=await e.signalRepository?.lidMapping?.getPNForLID?.(n);if(o){let r=oe(o)??"";if(r)return{fromJid:n,fromE164:r}}}catch{}return{fromJid:n,fromE164:oe(n)??""}}function uv(e){try{if(!S().clusterEnabled)return;let n=Om(e.message??void 0);if(!n)return;let o=Nd(n);if(!o)return;let r=e.key?.remoteJid??"",s=null;if(r&&!Lm(r)){let i=oe(r);i&&(s=`wa:${i}`)}Gd(o,s)}catch(t){M.warn({err:String(t)},"cluster footer observation failed")}}function Fm(e,t){let n=o=>{o.type==="notify"&&(async()=>{for(let r of o.messages){let s=r.key;if(!s||s.fromMe&&(uv(r),!Em(s.id)))continue;let i=s.remoteJid;if(!i||Lm(i)||i.toLowerCase().endsWith("@status")||i==="status@broadcast")continue;let l=Al(Om(r.message??void 0)),{fromJid:c,fromE164:u}=await cv(e,s),d=`wa:${c}`,p=Nm(r.message??void 0);if(p&&!l){(async()=>{try{let g=S(),y=await Im(e,r,d,g);await t({fromJid:c,fromE164:u,text:"",messageId:s.id??void 0,mediaSavedPath:y.path,mediaError:y.error})}catch(g){M.error({err:String(g),fromJid:c},"whatsapp media-only background task failed")}})();continue}let f,h;if(p){let g=S(),y=await Im(e,r,d,g);f=y.path,h=y.error}!l&&!f&&!h||await t({fromJid:c,fromE164:u,text:l,messageId:s.id??void 0,mediaSavedPath:f,mediaError:h})}})()};return e.ev.on("messages.upsert",n),()=>{e.ev.off("messages.upsert",n)}}xe();import wv from"node:fs";import dv from"node:process";import pv,{DisconnectReason as Hn,fetchLatestBaileysVersion as mv,makeCacheableSignalKeyStore as fv,useMultiFileAuthState as hv}from"@whiskeysockets/baileys";import gv from"qrcode-terminal";j();xe();var yv="0.1.0";function Il(e){return e?.error?.output?.statusCode}async function ii(e={}){re();let t=e.authDir??ae,n=e.verbose===!0,o=tu(),{state:r,saveCreds:s}=await hv(t),{version:i}=await mv(),a=pv({version:i,logger:o,printQRInTerminal:!1,browser:["omnish","cli",yv],auth:{creds:r.creds,keys:fv(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 p=dv.stdout,f=w(p,"\xB7".repeat(42));console.log(X(p,"Scan with WhatsApp \u2192 Linked devices")),console.log(f),gv.generate(d,{small:!0}),console.log(f)}if(c==="close"){let p=Il(u);n&&p===Hn.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 Po(e){try{e.end(new Error("omnish: socket closed"))}catch{e.ws?.close()}}function Ll(e){return e?.output?.statusCode??e?.status??e?.error?.output?.statusCode}function ai(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 Bn(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 Hm=3500,bv=400,_m=18e4,Wm=9e4,Dm=3e5;function Ol(e,t=Hm){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(`
|
|
287
296
|
|
|
288
|
-
`);i>Math.floor(t*.35)&&(r=o+i+2)}n.push(e.slice(o,r)),o=r}return n}function
|
|
297
|
+
`);i>Math.floor(t*.35)&&(r=o+i+2)}n.push(e.slice(o,r)),o=r}return n}function li(e){return new Promise(t=>setTimeout(t,e))}async function Um(e,t){await Promise.race([e,li(_m).then(()=>{M.warn({jid:t,ms:_m},"whatsapp outbound self-heal: prior send chain waited too long; continuing")})])}async function kv(e,t,n,o=3){let r;for(let s=1;s<=o;s+=1)try{let i=await Bn(e.sendMessage(t,{text:n}),Wm,`whatsapp sendMessage timed out after ${Wm}ms`);El(i);return}catch(i){r=i;let a=String(i);if(/not connected|closed|timed out|timeout/i.test(a)&&s<o){await li(800*s);continue}throw i}throw r}function vv(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 Sv(e,t,n,o=3){let r;for(let s=1;s<=o;s+=1)try{let i=await Bn(e.sendMessage(t,n),Dm,`whatsapp sendMedia timed out after ${Dm}ms`);El(i);return}catch(i){r=i;let a=String(i);if(/not connected|closed|timed out|timeout/i.test(a)&&s<o){await li(800*s);continue}throw i}throw r}function Bm(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=Um(a,r).then(async()=>{let c=Ol(i,Hm);for(let u=0;u<c.length;u+=1)await kv(e,r,c[u]??""),u<c.length-1&&await li(bv)}).catch(c=>{M.error({err:String(c),jid:r},"sendText failed")});n.set(r,l),await l.finally(()=>{n.get(r)===l&&n.delete(r)})},async sendMedia(r,s){let i=wv.readFileSync(s.absPath),a=vv(s,i),l=n.get(r)??Promise.resolve(),c=Um(l,r).then(async()=>{await Sv(e,r,a)}).catch(u=>{M.error({err:String(u),jid:r},"sendMedia failed")});n.set(r,c),await c.finally(()=>{n.get(r)===c&&n.delete(r)})}}}function jm(e){let t=e.trim();return t?/^\/id(?:@[\w_]+)?$/i.test(t):!1}function Gm(e){let t=String(e).replace(/\D/g,"");return`Your Telegram user id: ${t}
|
|
289
298
|
Add to allowlist on this gateway host:
|
|
290
|
-
omnish allow tg:${t}`}var
|
|
291
|
-
`));if(g&&
|
|
299
|
+
omnish allow tg:${t}`}var Tv=400;async function zm(e,t,n,o){let r=t(),s=await Pm(e,o.fileId,r.fileReceiveMaxBytes);if("error"in s)return{mediaError:s.error};let i;try{i=nn(r,n)}catch(l){return{mediaError:String(l)}}let a=$o(i,n,o.baseName);try{return xv.writeFileSync(a,s.buffer,{mode:384}),{mediaSavedPath:a}}catch{return{mediaError:"Could not write media to inbox."}}}function $v(e){return new Promise(t=>setTimeout(t,e))}async function Nl(e,t,n,o={}){let r=new Cv(e);await r.api.deleteWebhook({drop_pending_updates:!1});let s=new Map,i=o.decorate??(u=>u);async function a(u,d){let p=Math.min(t().appsMaxWaChars,4096),f=ue(d,"telegram"),h=i(f.text,Ya(u)),g=f.parseModeHtml,b=(s.get(u)??Promise.resolve()).then(async()=>{let k=Ol(h,p);for(let x=0;x<k.length;x+=1){let T=k[x]??"",O=g?{parse_mode:"HTML"}:void 0;try{await r.api.sendMessage(u,T,O)}catch(C){if(g){M.warn({err:String(C),chatId:u},"telegram HTML send failed; retrying plain");let N=T.replace(/<[^>]+>/g,"");await r.api.sendMessage(u,N)}else throw C}x<k.length-1&&await $v(Tv)}}).catch(k=>{M.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 p=new Rv(d.absPath,d.displayName),f=d.caption,g=(s.get(u)??Promise.resolve()).then(async()=>{switch(d.category){case"image":await r.api.sendPhoto(u,p,f?{caption:f}:void 0);break;case"video":await r.api.sendVideo(u,p,f?{caption:f}:void 0);break;case"audio":await r.api.sendAudio(u,p,f?{caption:f}:void 0);break;case"document":await r.api.sendDocument(u,p,f?{caption:f}:void 0);break}}).catch(y=>{M.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,p=u.message;if(!d||d.type!=="private"||!u.from||!p)return;let f="text"in p&&p.text?p.text:"",h="caption"in p&&p.caption?p.caption:"",g=Al([f,h].filter(Boolean).join(`
|
|
300
|
+
`));if(g&&jm(g)){let C=u.from.id;await r.api.sendMessage(d.id,Gm(C));return}let y=$m(p),b=Ya(d.id),k=d.id,x=async C=>{if(C.kind==="file")await l(k,C.spec);else if(C.kind==="files")for(let N of C.specs)await l(k,N);else if(C.kind==="texts")for(let N of C.bodies)await a(k,N);else if(C.kind==="bundle"){for(let N of C.texts??[])await a(k,N);for(let N of C.files??[])await l(k,N)}else C.kind==="text"&&await a(k,C.body)};if(y&&!g){(async()=>{try{let C=await zm(r,t,b,y);if(!C.mediaSavedPath&&!C.mediaError)return;await n({peerKey:b,text:"",tgChatId:d.id,tgReplyToMessageId:p.message_id,mediaSavedPath:C.mediaSavedPath,mediaError:C.mediaError},x)}catch(C){M.error({err:String(C),chatId:k},"telegram media-only background task failed")}})();return}let T,O;if(y){let C=await zm(r,t,b,y);T=C.mediaSavedPath,O=C.mediaError}!g&&!T&&!O||await n({peerKey:b,text:g,tgChatId:d.id,tgReplyToMessageId:p.message_id,mediaSavedPath:T,mediaError:O},x)}),r.catch(u=>{M.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 Mv from"node:fs";import Pv from"node:path";function Ev(e){let t=Pv.join(e,"creds.json");try{let n=Mv.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 Jm(e){let t=Ev(e);if(!t)return null;let n=t.phoneNumber?.trim();if(n){let s=oe(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=oe(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 ui from"node:fs";import ct from"node:process";j();j();import qm from"node:fs";var Av="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 ci(e){let t=String(e).toLowerCase();return t.includes("401")||t.includes("logged out")?!0:Ll(e)===Hn.loggedOut}function Iv(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 Fl(e){let t=!1;for(let n=0;n<3;n++){if(e.signal?.aborted)throw new Error("Pairing cancelled.");let o=await ii({authDir:e.authDir,printQr:e.printQr===!0,verbose:e.verbose===!0,onQr:e.onQr});e.onSocketReady?.(o);try{let r=Bn(ai(o),6e5,Av);e.signal?await Promise.race([r,Iv(e.signal)]):await r;return}catch(r){if(r instanceof Error&&r.message==="Pairing cancelled.")throw r;if(Ll(r)===Hn.restartRequired&&!t){t=!0,e.onRestart515?.(),await new Promise(i=>setTimeout(i,1500));continue}throw r}finally{e.onSocketClosed?.(),Po(o)}}throw new Error("Pairing failed after restart (515) retries.")}async function Km(e){let t=e.authDir??ae;re();for(let n=1;n<=2;n++)try{await Fl({...e,authDir:t});return}catch(o){if(o instanceof Error&&o.message==="Pairing cancelled.")throw o;if(n===1&&ci(o)){qm.rmSync(t,{recursive:!0,force:!0}),qm.mkdirSync(t,{recursive:!0,mode:448});continue}throw o}}async function Lv(e,t){let n=ct.stdout;console.log(`
|
|
292
301
|
${Ce(n,"omnish link")} ${w(n,"\u2014 QR appears below; scan from WhatsApp \u2192 Linked devices.")}
|
|
293
|
-
`),await
|
|
294
|
-
${
|
|
295
|
-
`)}})}async function
|
|
296
|
-
`));for(let o=1;o<=2;o++)try{await
|
|
297
|
-
${
|
|
298
|
-
`);return}catch(r){if(o===1&&
|
|
299
|
-
${
|
|
300
|
-
${
|
|
301
|
-
`),
|
|
302
|
-
${
|
|
302
|
+
`),await Fl({authDir:e,verbose:t,printQr:!0,onRestart515:()=>{console.warn(`
|
|
303
|
+
${H(ct.stderr,"WhatsApp requested a restart after pairing (code 515). This is normal. Opening a new connection\u2026")}
|
|
304
|
+
`)}})}async function Ym(e={}){let t=e.authDir??ae,n=e.verbose===!0;re(),e.force&&(ui.rmSync(t,{recursive:!0,force:!0}),ui.mkdirSync(t,{recursive:!0,mode:448}),console.log(`${we(ct.stdout,"Cleared saved session (--force).")} ${w(ct.stdout,"Requesting a new QR\u2026")}
|
|
305
|
+
`));for(let o=1;o<=2;o++)try{await Lv(t,n),console.log(`
|
|
306
|
+
${fe(ct.stdout,"Linked.")} ${v(ct.stdout,"Session saved. You can run")} ${fe(ct.stdout,"omnish run")} ${v(ct.stdout,"now.")}
|
|
307
|
+
`);return}catch(r){if(o===1&&ci(r)){console.warn(`
|
|
308
|
+
${H(ct.stderr,"WhatsApp returned logged-out (401). This often happens after Ctrl+C during link or corrupt auth files.")}
|
|
309
|
+
${H(ct.stderr,"Clearing auth dir and retrying once with a fresh QR\u2026")}
|
|
310
|
+
`),ui.rmSync(t,{recursive:!0,force:!0}),ui.mkdirSync(t,{recursive:!0,mode:448});continue}throw ci(r)&&console.error(`
|
|
311
|
+
${R(ct.stderr,"Still failing after a clean auth directory. Try:")}
|
|
303
312
|
${w(ct.stderr,` pnpm approve-builds && pnpm install
|
|
304
313
|
(pnpm may have skipped Baileys/sharp/protobuf build scripts.)
|
|
305
314
|
Then: omnish link --force
|
|
306
|
-
`)}`),r}}
|
|
307
|
-
`,{mode:384})}function
|
|
308
|
-
`);if(i===-1)return;let a=r.slice(0,i).trim();r=r.slice(i+1);let l=
|
|
309
|
-
`),o.end()})}),o.on("error",()=>{})});n.listen(0,"127.0.0.1",()=>{let o=n.address();if(!o||typeof o=="string"){
|
|
315
|
+
`)}`),r}}j();import{spawn as Ov,spawnSync as Nv}from"node:child_process";import Eo from"node:fs";import Fv from"node:path";import Ao from"node:process";function di(e,t={}){re(),U(Fv.dirname(e));let n=Ao.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=Eo.openSync(e,"a"),s=Ov(Ao.execPath,[n,...o],{detached:!0,stdio:["ignore",r,r],env:{...Ao.env,OMNISH_BACKGROUND_GATEWAY:"1"}});return Eo.closeSync(r),s.unref(),s.pid?{ok:!0,pid:s.pid}:{ok:!1,message:"Failed to start background gateway."}}function pi(){if(re(),!Eo.existsSync(me))return{outcome:"no_pidfile"};let e=Eo.readFileSync(me,"utf8").trim(),t=Number(e);if(!Number.isFinite(t)||t<=0){try{Eo.unlinkSync(me)}catch{}return{outcome:"invalid_pidfile"}}try{Ao.kill(t,0)}catch{try{Eo.unlinkSync(me)}catch{}return{outcome:"stale_cleaned",pid:t}}try{return Ao.kill(t,"SIGTERM"),{outcome:"sent_signal",pid:t}}catch(n){return Ao.platform==="win32"&&Nv("taskkill",["/PID",String(t),"/T"],{windowsHide:!0}).status===0?{outcome:"taskkill_ok",pid:t}:{outcome:"failed",message:`could not signal process: ${String(n)}`}}}import Qm from"node:crypto";import _l from"node:fs";import _v from"node:net";ot();xe();j();var vr=null;function Wv(){try{let e=_l.readFileSync(Vn,"utf8"),t=JSON.parse(e);return typeof t.token=="string"?t.token:""}catch{return""}}function Dv(e){_l.writeFileSync(Vn,JSON.stringify(e,null,2)+`
|
|
316
|
+
`,{mode:384})}function Uv(){try{_l.unlinkSync(Vn)}catch{}}async function Hv(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(),i=rl(n,s),a=ht(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:r};if(e.channel==="whatsapp"){let c=t.getWaOutbound(),u=typeof e.e164=="string"?e.e164.trim():"";if(!u.startsWith("+"))return{ok:!1,error:"WhatsApp destination must be E.164 (e.g. +15551234567)."};let d=Vt(u);if(!c){let f=t.sendPlatformMedia;if(!f)return{ok:!1,error:"WhatsApp outbound is not connected."};try{return await f(`wa:${d}`,l),{ok:!0}}catch(h){return{ok:!1,error:String(h)}}}let p=n.gatewayMode;if(p!=="whatsapp"&&p!=="both")return{ok:!1,error:"gatewayMode does not include WhatsApp."};try{return await c.sendMedia(d,l),{ok:!0}}catch(f){return{ok:!1,error:String(f)}}}if(e.channel==="telegram"){let c=t.getTgSendMedia();if(!c){let d=t.sendPlatformMedia;if(!d)return{ok:!1,error:"Telegram outbound is not connected."};if(!Number.isFinite(e.chatId))return{ok:!1,error:"Invalid Telegram chat id."};let p=`tg:${e.chatId}`;try{return await d(p,l),{ok:!0}}catch(f){return{ok:!1,error:String(f)}}}let u=n.gatewayMode;if(u!=="telegram"&&u!=="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 c(e.chatId,l),{ok:!0}}catch(d){return{ok:!1,error:String(d)}}}return{ok:!1,error:"Unknown channel."}}async function Bv(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=Vt(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=Vt(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,m(o)),{ok:!0}}catch(i){return{ok:!1,error:String(i)}}}return{ok:!1,error:"Unknown channel."}}async function jv(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||!Qm.timingSafeEqual(r,s))return{ok:!1,error:"Unauthorized."}}catch{return{ok:!1,error:"Unauthorized."}}return o.op==="sendMedia"?Hv(o,t):o.op==="sendText"?Bv(o,t):o.op==="sendPeerText"?zv(o,t):o.op==="sendPeerMedia"?Gv(o,t):{ok:!1,error:"Unsupported operation."}}async function Gv(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(),a=rl(r,i),l=ht(o,a);if("error"in l)return{ok:!1,error:l.error};let c={absPath:l.absPath,category:l.category,mimetype:l.mimetype,displayName:l.displayName,caption:s};if(t.sendPeerMedia)try{return await t.sendPeerMedia(n,c),{ok:!0}}catch(u){return{ok:!1,error:String(u)}}if(t.sendPlatformMedia)try{return await t.sendPlatformMedia(n,c),{ok:!0}}catch(u){return{ok:!1,error:String(u)}}if(n.startsWith("wa:")){let u=t.getWaOutbound();if(!u)return{ok:!1,error:"WhatsApp outbound is not connected."};let d=r.gatewayMode;if(d!=="whatsapp"&&d!=="both")return{ok:!1,error:"gatewayMode does not include WhatsApp."};try{return await u.sendMedia(n.slice(3),c),{ok:!0}}catch(p){return{ok:!1,error:String(p)}}}if(n.startsWith("tg:")){let u=t.getTgSendMedia();if(!u)return{ok:!1,error:"Telegram outbound is not connected."};let d=r.gatewayMode;if(d!=="telegram"&&d!=="both")return{ok:!1,error:"gatewayMode does not include Telegram."};let p=Number(n.slice(3));if(!Number.isFinite(p))return{ok:!1,error:"Invalid Telegram peer key."};try{return await u(p,c),{ok:!0}}catch(f){return{ok:!1,error:String(f)}}}return{ok:!1,error:"Unknown peer key prefix."}}async function zv(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,m(o)),{ok:!0}}catch(l){return{ok:!1,error:String(l)}}}return{ok:!1,error:"Unknown peer key prefix."}}function mi(e){if(vr)return;let t=Qm.randomBytes(32).toString("hex"),n=_v.createServer(o=>{let r="";o.setTimeout(12e4),o.on("data",s=>{r+=s.toString("utf8");let i=r.indexOf(`
|
|
317
|
+
`);if(i===-1)return;let a=r.slice(0,i).trim();r=r.slice(i+1);let l=Wv();jv(a,e,l).then(c=>{o.write(`${JSON.stringify(c)}
|
|
318
|
+
`),o.end()})}),o.on("error",()=>{})});n.listen(0,"127.0.0.1",()=>{let o=n.address();if(!o||typeof o=="string"){M.error("gateway control: could not read listen address");return}let r={token:t,host:o.address,port:o.port};Dv(r),M.info({port:o.port},"gateway control listening")}),n.on("error",o=>{M.error({err:String(o)},"gateway control server error")}),vr=n}function Io(){if(vr){try{vr.close()}catch{}vr=null,Uv()}}xe();import Jv from"node:crypto";import qv from"node:http";var Kv=256*1024;function Yv(e,t){if(!e||!t)return!1;try{let n=Buffer.from(e,"utf8"),o=Buffer.from(t,"utf8");return n.length===o.length&&Jv.timingSafeEqual(n,o)}catch{return!1}}function Qv(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??"",p=e.repository?.full_name??"?";return`[${t}] ${p} \u2014 ${l}
|
|
310
319
|
result: ${c}
|
|
311
320
|
branch: ${u}${d?`
|
|
312
321
|
${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}
|
|
313
322
|
status: ${l}
|
|
314
323
|
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
|
|
317
|
-
${w(n,`${i.localHost}:${i.localPort}`)}`);return}if(t.kind==="stop"){let s=await
|
|
318
|
-
`,{mode:384})}function
|
|
319
|
-
`).replace(/\n/g," ").trim();return t?t.length>
|
|
320
|
-
Script: ${s.scriptPath}`,u=["*Service status*","",`platform: ${
|
|
321
|
-
`),d=["<b>Service status</b>","",`<code>${
|
|
322
|
-
`);return
|
|
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,
|
|
324
|
+
`)}var Wl=null;function fi(e,t){if(Wl)return{stop:()=>{}};let n=qv.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(!Yv(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>Kv){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 f=new URL(o.url??"/",`http://${o.headers.host??"localhost"}`).searchParams.get("source")??o.headers["x-webhook-source"]??void 0;f&&(d.source=f);let h=typeof d.peerKey=="string"&&d.peerKey||t.getDefaultPeerKey();if(!h){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=Qv(d);t.sendToPeer(h,g).then(()=>{r.writeHead(200,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!0}))},y=>{M.warn({err:String(y)},"webhook: sendToPeer failed"),r.writeHead(502,{"Content-Type":"application/json"}),r.end(JSON.stringify({ok:!1,error:"Failed to deliver message"}))})})});return n.listen(e.port,e.host,()=>{let o=n.address(),r=typeof o=="object"&&o?o.port:e.port;M.info({port:r,host:e.host},"webhook receiver listening")}),n.on("error",o=>{M.error({err:String(o)},"webhook receiver error")}),Wl=n,{stop:()=>{try{n.close()}catch{}Wl=null}}}ce();import Dx from"node:crypto";import xc from"node:fs";xe();import Vv from"node:fs";function Vm(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=nn(e,t)}catch(a){return{mediaError:String(a)}}let i=$o(s,t,n.name);try{return Vv.writeFileSync(i,r,{mode:384}),{mediaSavedPath:i}}catch{return{mediaError:"Could not write media to inbox."}}}j();xr();xe();import Kl from"ws";import rS from"ws";var oS=["/platform/device","/control/device"];function wi(e){let t=e.replace(/\/$/,"");return oS.map(n=>{let o=new URL(n,t);return o.protocol=o.protocol==="https:"?"wss:":"ws:",o.toString()})}function sS(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 tf(e,t){let n=wi(e),o=null;for(let s of n)try{return{ws:await new Promise((a,l)=>{let c=new rS(s,{headers:{Authorization:`Bearer ${t}`},maxPayload:up()});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)),sS(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 iS=1e3,aS=6e4,bi=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 tf(t.platformUrl,t.token);this.ws=o,r!=="/platform/device"&&M.info({pathname:r},"platform device websocket connected via fallback path"),o.on("message",a=>{this.handleMessage(a.toString())}),o.on("close",()=>{this.stopped||(M.warn("platform device websocket closed; scheduling reconnect"),this.scheduleReconnect())}),o.on("error",a=>{M.warn({err:String(a)},"platform device websocket error")});let s=setTimeout(()=>{this.registerReject?.(new Error("Platform device register timeout (15s)"))},15e3);this.sendRaw({type:"register",...t.deviceId?{deviceId:t.deviceId}:{},label:process.env.OMNISH_DEVICE_LABEL?.trim()||"default"});let i=await n;return clearTimeout(s),this.registeredDeviceId=i.deviceId,this.reconnectAttempt=0,this.pingTimer&&clearInterval(this.pingTimer),this.pingTimer=setInterval(()=>{this.sendRaw({type:"ping"})},3e4),this.flushOutboundQueue(),i}scheduleReconnect(){if(this.stopped||this.reconnectTimer)return;this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null),this.ws=null;let t=Math.min(iS*2**this.reconnectAttempt,aS);this.reconnectAttempt+=1,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,!this.stopped&&this.connectOnce().catch(n=>{M.warn({err:String(n)},"platform device reconnect failed"),this.scheduleReconnect()})},t),this.reconnectTimer.unref?.()}async handleMessage(t){let n=pp(t);if(n){if(n.type==="registered"&&"deviceId"in n){n.account&&(this.registeredAccount=n.account),this.registerResolve?.({deviceId:n.deviceId,...n.account?{account:n.account}:{}}),this.registerResolve=null,this.registerReject=null;return}if(n.type==="message"){await this.options.onMessage(n);return}if(n.type==="reply_error"){await this.options.onReplyError?.(n.peerKey,n.error,n.messageId);return}n.type==="error"&&(M.warn({message:n.message},"platform error"),this.registerReject?.(new Error(n.message)),this.registerResolve=null,this.registerReject=null)}}sendReply(t,n,o,r){this.enqueue({type:"reply",peerKey:t,...n?{body:n}:{},...o?{messageId:o}:{},...r?.length?{files:r}:{}})}sendRoutedReply(t,n){this.enqueue({type:"reply",peerKey:t,...n})}enqueue(t){if(this.ws?.readyState===Kl.OPEN){this.sendRaw(t);return}if(this.outboundQueue.length>=dp){M.warn("platform outbound queue full; dropping reply");return}this.outboundQueue.push(t)}flushOutboundQueue(){for(;this.outboundQueue.length>0&&this.ws?.readyState===Kl.OPEN;){let t=this.outboundQueue.shift();t&&this.sendRaw(t)}}sendRaw(t){this.ws?.readyState===Kl.OPEN&&this.ws.send(JSON.stringify(t))}stop(){this.stopped=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null);try{this.ws?.close()}catch{}this.ws=null,this.outboundQueue.length=0}};import Xl from"node:readline/promises";import{stdin as Zl,stdout as Ti}from"node:process";ce();var lS=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;function of(e){let t=e.trim().toLowerCase();return t?t.length>63?"Tunnel name must be at most 63 characters.":lS.test(t)?null:"Tunnel name may only use letters, digits, and hyphens.":"Tunnel name must not be empty."}function rf(e){let t=Number.parseInt(e,10);return!Number.isInteger(t)||t<1||t>65535?null:t}function nf(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=rf(a);if(l===null)return{kind:"error",message:"Port must be an integer between 1 and 65535."};if(r){let c=of(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 Yl(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"?nf("http",n):o==="tcp"?nf("tcp",n):{kind:"error",message:`Unknown tunnel subcommand "${t}". Try: omnish tunnel help`}}function cS(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 sf(e){let t=e.trim();if(!t||t==="help")return{kind:"help"};let n=cS(t),o=n[0]?.toLowerCase();if(o==="login"||o==="logout"||o==="status"||o==="signup")return Yl(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=rf(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=of(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 uS from"ws";import{URL as af}from"node:url";function No(e){let t=new af(e);return t.protocol=t.protocol==="https:"?"wss:":"ws:",(!t.pathname||t.pathname==="/")&&(t.pathname="/control"),t.toString()}function lf(e){return new af("/health",e).toString()}async function ki(e,t,n=1e4){let o=t.trim();if(!o)return{ok:!1,healthOk:!1,controlOk:!1,error:"Tunnel token is missing."};let r=lf(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=No(e),l=!1;try{await new Promise((c,u)=>{let d=new uS(a,{headers:{Authorization:`Bearer ${o}`}}),p=setTimeout(()=>{d.terminate(),u(new Error("WSS auth timeout"))},n);d.once("message",f=>{clearTimeout(p);try{let h=JSON.parse(f.toString());if(h.type!=="auth_ok"){u(new Error(`Expected auth_ok, got ${h.type??"?"}`));return}d.close(),c()}catch(h){u(h)}}),d.once("error",f=>{clearTimeout(p),u(f)})}),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}}Tn();$n();$n();Tn();import df from"node:crypto";import dS from"node:http";import pS from"node:net";import kn from"ws";function vi(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 cf(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 Ql(e){try{let t=JSON.parse(e);return!t||typeof t!="object"||typeof t.type!="string"?null:t}catch{return null}}function Fo(e){return JSON.stringify(e)}function mS(e){let t=df.createHash("sha1").update(e,"utf8").digest("hex").slice(0,8);return Number.parseInt(t,16)>>>0}var Si=class{constructor(t){this.opts=t;let n=df.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=No(this.opts.relayUrl),n=new kn(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===kn.OPEN&&n.send(Fo({type:"ping"}))},3e4),n.send(Fo({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=Ql(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=Ql(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=cf(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!==kn.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=dS.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(Fo({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(Fo({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!==kn.OPEN)return;let o=mS(t);this.tcpStreamIds.set(t,o);let r=pS.connect({host:this.opts.expose.host,port:this.opts.expose.port});this.tcpStreams.set(t,r),r.on("data",s=>{n.readyState===kn.OPEN&&n.send(vi(2,o,Buffer.isBuffer(s)?s:Buffer.from(s)))}),r.on("close",()=>{n.readyState===kn.OPEN&&n.send(vi(3,o)),this.tcpStreams.delete(t),this.tcpStreamIds.delete(t)}),r.on("error",()=>{n.readyState===kn.OPEN&&n.send(vi(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===kn.OPEN&&(t.send(Fo({type:"unregister",id:this.record.id})),t.close()),this.cleanupTcpStreams(),this.setStatus("stopped")}};var xi=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=$t();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 Si({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 pf(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 Ci(e,t){let n=new URL("/auth/signup",e).toString();return pf(n,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:t.password})}function Ri(e,t){let n=new URL("/auth/login",e).toString();return pf(n,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:t.password})}var _o=new xi;function Gn(){return _o}function fS(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(`
|
|
325
|
+
`))}async function hS(){let e=Xl.createInterface({input:Zl,output:Ti});try{return(await e.question("Tunnel token: ")).trim()}finally{e.close()}}async function mf(e){let t=Yl(e),n=Ti,o=process.stderr;if(t.kind==="help"){fS(n);return}if(t.kind==="error"){console.error(R(o,t.message)),process.exitCode=1;return}let r=S();if(t.kind==="signup"){let s=t.relayUrl||mt(r.tunnelRelayUrl||Ee),i=Xl.createInterface({input:Zl,output:Ti});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(R(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(R(o,"Password must be at least 8 characters.")),process.exitCode=1;return}let u=await Ci(s,{...a?{email:a}:{},...l?{phone:l}:{},password:c});if(!u.ok){console.error(R(o,u.error)),process.exitCode=1;return}Tt({token:u.token,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(H(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=Xl.createInterface({input:Zl,output:Ti});try{let c=t.password||(await l.question("Password: ")).trim(),u=await Ri(a,{...t.email?{email:t.email}:{},...t.phone?{phone:t.phone}:{},password:c});if(!u.ok){console.error(R(o,u.error)),process.exitCode=1;return}Tt({token:u.token,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(H(n,"Logged in. Token saved."))}finally{l.close()}return}let i=t.token?.trim()||await hS();if(!i){console.error(R(o,"Tunnel token is required.")),process.exitCode=1;return}Tt({token:i,...t.relayUrl?{relayUrl:t.relayUrl}:{}}),console.log(H(n,"Tunnel token saved."));return}if(t.kind==="logout"){ps(),console.log(H(n,"Tunnel token removed."));return}if(t.kind==="status"){let s=t.relayUrl||mt(r.tunnelRelayUrl||Ee),i=$t(),a=!!process.env.OMNISH_TUNNEL_TOKEN?.trim(),l=await ki(s,i);console.log(`${w(n,"relay:")} ${v(n,s)}`),console.log(`${w(n,"token:")} ${v(n,i?`configured${a?" (OMNISH_TUNNEL_TOKEN)":""}`:R(o,"missing"))}`),console.log(`${w(n,"health:")} ${l.healthOk?v(n,`ok${l.healthVersion?` (${l.healthVersion})`:""}`):R(o,"fail")}`),console.log(`${w(n,"control:")} ${l.controlOk?v(n,"auth ok"):R(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(H(n,"(no active tunnels)"));return}for(let i of s)console.log(`${v(n,i.id)} ${i.kind} ${i.status} ${i.publicUrl||"(pending)"}
|
|
326
|
+
${w(n,`${i.localHost}:${i.localPort}`)}`);return}if(t.kind==="stop"){let s=await _o.stop(t.target);if(!s){console.error(R(o,`No active tunnel matched "${t.target}".`)),process.exitCode=1;return}console.log(H(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(H(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 ff(e){let t=e.trim().match(/^(\d+)\.(\d+)\.(\d+)/);return t?[Number(t[1]),Number(t[2]),Number(t[3])]:null}function hf(e,t){let n=ff(e),o=ff(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 gS="https://registry.npmjs.org",gf=null,ec=0;function Cr(){return gf}function yS(e){let t=e.trim();return t.length<1||t.length>214?!1:!/\s/.test(t)}function yf(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 wS(e){let t=e.trim(),n=`${gS}/${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 bS(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=yf(c)),{message:a,link:l}}catch(t){return{error:`updateInfoUrl: ${String(t)}`}}}function kS(e){return!Number.isFinite(e)||e<36e5?36e5:e>6048e5?6048e5:Math.floor(e)}async function Rr(e,t){let n=yS(t.updateCheckPackageName)?t.updateCheckPackageName.trim():"omnish",o=await wS(n),r="version"in o?o.version:null,s="error"in o?o.error:null,i=!1;r&&!s&&(i=hf(e,r)<0);let a=null,l=null,c=null,u=yf(t.updateInfoUrl);if(u){let p=await bS(u);"error"in p?c=p.error:(a=p.message,l=p.link)}let d={runningVersion:e,checkedAtIso:new Date().toISOString(),registryPackage:n,registryLatest:r,registryError:s,updateAvailable:i,infoMessage:a,infoLink:l,infoError:c};return gf=d,d}function $i(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 Mi(e){let t=!1,n=async()=>{if(t)return;let s=e.getConfig();if(!s.updateCheckEnabled)return;let i=kS(s.updateCheckIntervalMs),a=Date.now();if(!(ec!==0&&a-ec<i)){ec=a;try{let l=await Rr(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)}}ce();import wf from"node:path";import{glob as vS,stat as SS}from"node:fs/promises";function Pi(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 xS(e){return/[*?[]/.test(e)}function CS(e){return e.split(",").map(t=>t.trim()).filter(t=>t.length>0)}async function Tr(e,t){let n=CS(t),o=new Set,r=[];for(let s of n){if(xS(s)){for await(let a of vS(s,{cwd:e,withFileTypes:!1})){let l=wf.resolve(e,a);o.has(l)||(o.add(l),r.push(l))}continue}let i=wf.resolve(e,s);o.has(i)||(o.add(i),r.push(i))}return r}async function $r(e){for(let t of e)try{if(!(await SS(t)).isFile())return{ok:!1,error:`Not a file: ${t}`}}catch{return{ok:!1,error:`File not found: ${t}`}}return{ok:!0}}j();import vf from"node:fs";import RS from"node:path";var zn="__omnish_shortcuts_global__",bf=500,TS=/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/,$S=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"]),Mr=new Map,kf=!1;function MS(){try{let e=vf.readFileSync(Ur,"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())}Mr.set(n,r)}}catch{}}function Ei(){U(RS.dirname(Ur));let e={};for(let[t,n]of Mr)Object.entries(n).length>0&&(e[t]={...n});vf.writeFileSync(Ur,JSON.stringify(e,null,2)+`
|
|
327
|
+
`,{mode:384})}function Ai(){kf||(MS(),kf=!0)}function PS(e){return $S.has(e.trim().toLowerCase())}function bt(e){let t=e.trim();if(!t)return{ok:!1,error:"Name is empty."};let n=t.toLowerCase();return TS.test(n)?PS(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 tc(e){let t=e.replace(/\r\n/g,`
|
|
328
|
+
`).replace(/\n/g," ").trim();return t?t.length>bf?{ok:!1,error:`Body too long (max ${bf} characters).`}:{ok:!0,body:t}:{ok:!1,error:"Body is empty."}}function It(e,t){Ai();let n=Mr.get(e);return!n&&t&&(n={},Mr.set(e,n)),n??{}}function Sf(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 nc(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 oc(e){let t=nc(e);return{scope:t.scope,remainder:t.remainder}}function xf(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 ES(e){let t=e.trim().toLowerCase();if(t==="--global"||t==="-g")return"global";if(t==="--chat"||t==="-p")return"chat"}function Cf(e){let t=e.trim().match(/^(\S+)\s+(--global|-g|--chat|-p)\s*$/i);if(!t?.[1]||!t[2])return;let n=ES(t[2]);if(n)return{name:t[1],target:n}}function AS(e,t){Ai();let n=e,o=It(n,!1),r=It(zn,!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 Rf(e,t="merged"){return AS(e,t)}function IS(e,t){let n=t.trim().toLowerCase(),o=It(e,!1)[n];return o!==void 0?o:It(zn,!1)[n]}function Ii(e,t){let n=bt(t);if(!n.ok)return;let o=n.normalized,r=It(e,!1)[o];if(r!==void 0)return{name:o,body:r,scope:"chat"};let s=It(zn,!1)[o];if(s!==void 0)return{name:o,body:s,scope:"global"}}function Kt(e,t,n){let o=n.trim().toLowerCase(),r=e==="global"?zn:t;return It(r,!1)[o]}function Pr(e,t,n,o="chat"){let r=bt(t);if(!r.ok)throw new Error(r.error);let s=tc(n);if(!s.ok)throw new Error(s.error);let i=o==="global"?zn:e,a=It(i,!0);a[r.normalized]=s.body,Ei()}function Tf(e,t,n="chat"){let o=t.trim().toLowerCase(),r=n==="global"?zn:e;Ai();let s=Mr.get(r);return!s||!(o in s)?!1:(delete s[o],Ei(),!0)}function rc(e,t,n){let o=bt(t);if(!o.ok)return{ok:!1,error:o.error};let r=o.normalized,s=e;Ai();let i=It(s,!0),a=It(zn,!0),l=i[r],c=a[r];if(n==="global"){if(l!==void 0){let u=tc(l);return u.ok?(a[r]=u.body,delete i[r],Ei(),{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=tc(c);return u.ok?(i[r]=u.body,delete a[r],Ei(),{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}.`}}var $f="OMNISH_INPUT",LS=new RegExp(`\\$${$f}\\b`,"g");function OS(e){return e.includes(`$${$f}`)}function NS(e,t){return e.replace(LS,t)}function FS(e){let t=e.trim();if(!t)return null;let n=/^(\S+)(?:\s+([\s\S]+))?$/.exec(t);if(!n?.[1])return null;let o=bt(n[1]);if(!o.ok)return null;let r=n[2]?.trim();return r!==void 0&&r.length>0?{name:o.normalized,input:r}:{name:o.normalized}}function Mf(e,t,n=0){let o=FS(t);if(!o)return null;let r=IS(e,o.name);if(r===void 0)return null;if(OS(r)){if(o.input===void 0)return{ok:!1,error:`Shortcut "${o.name}" expects input (text after the name on the same line). Example: !${o.name} <text\u2026> or /${o.name} <text\u2026>`};let s=vo(o.input,n);return s.ok?{ok:!0,text:NS(r,s.task)}:{ok:!1,error:s.error}}return{ok:!0,text:r}}import Pf from"node:fs";var Ef=64,_S=1024*1024;function Af(e){return e.fileReceiveMaxBytes>0?e.fileReceiveMaxBytes:_S}function If(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>Ef)return{ok:!1,error:`Too many jobs (max ${Ef}).`};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 Lf(e,t,n){let o=[];for(let r=0;r<n.length;r++){let{recipe:s,task:i}=n[r],a=Ze(e,t,s);if(!a)return{ok:!1,error:`Job ${r+1}: unknown recipe "${s}".`};let l=a.taskEnv??"OMNISH_TASK";if(!So(a.command,l))return{ok:!1,error:`Job ${r+1}: recipe "${s}" command must reference "$${l}".`};let c=vo(i,t.recipesMaxTaskChars);if(!c.ok)return{ok:!1,error:`Job ${r+1}: ${c.error}`};let u=a.promptTemplate?Us(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 sc(e,t){let n;try{n=Pf.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:Pf.readFileSync(e,"utf8")}}catch(o){return{ok:!1,error:String(o)}}}j();import Of from"node:fs";import Wo from"node:process";var WS=120;function Yt(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function Nf(e){let t=e.trim();return t==="/service"||t.startsWith("/service ")?t.slice(8).trim():null}async function Ff(e,t){let n=t.trim().split(/\s+/),o=(n[0]??"").toLowerCase();if(!t.trim()||o==="help")return Q(Qu(e));if(o==="status"){let s=zt(),i=(()=>{try{return Of.existsSync(me)?`gateway.pid: ${Of.readFileSync(me,"utf8").trim()}`:"gateway.pid: (missing)"}catch(p){return`gateway.pid: (read error: ${String(p)})`}})(),a=Wo.env.OMNISH_BACKGROUND_GATEWAY==="1"?"This process: background gateway (OMNISH_BACKGROUND_GATEWAY=1).":"This process: foreground gateway session.",l=typeof Wo.env.OMNISH_HOME=="string"&&Wo.env.OMNISH_HOME.trim()?`OMNISH_HOME env: ${Wo.env.OMNISH_HOME.trim()}`:"OMNISH_HOME env: (not set \u2014 using default data dir)",c=s.error?s.error:`Node: ${s.nodePath}
|
|
329
|
+
Script: ${s.scriptPath}`,u=["*Service status*","",`platform: ${Wo.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(`
|
|
330
|
+
`),d=["<b>Service status</b>","",`<code>${Yt(Wo.platform)}</code>`,`<br/><code>${Yt(a)}</code>`,`<br/><code>${Yt(l)}</code>`,`<br/>data dir: <code>${Yt(D)}</code>`,`<br/><code>${Yt(i)}</code>`,`<br/>default log: <code>${Yt(je)}</code>`,"",`<pre>${Yt(c)}</pre>`,"",e.serviceInstallFromChat?"Install from chat: enabled.":"Install from chat: off \u2014 <code>/config set serviceInstallFromChat true</code>."].join(`
|
|
331
|
+
`);return ye(u,d)}if(o==="instructions"){let s=zt();if(s.error)return m(s.error);let i=ys(s);return m(`*Install hints*
|
|
332
|
+
|
|
333
|
+
${i}`)}if(o==="logs"){let s=n.length>=2?Number.parseInt(n[1],10):80,i=Number.isFinite(s)&&s>0?Math.min(s,WS):80,a=Fs(je,i),l=[`*Gateway log* (last ${i} lines)
|
|
325
334
|
${je}
|
|
326
335
|
`,"```",a,"```"].join(`
|
|
327
|
-
`),c=`<b>Gateway log</b> (last ${i} lines)<br/><code>${
|
|
336
|
+
`),c=`<b>Gateway log</b> (last ${i} lines)<br/><code>${Yt(je)}</code><pre>${Yt(a)}</pre>`;return ye(l,c)}if(o==="install"){if(!e.serviceInstallFromChat)return m("Install from chat is disabled. Same trust as shell \u2014 enable with:\n`/config set serviceInstallFromChat true`\nThen `/service install` again.");let s=Ls();return m(s.ok?`*Installed*
|
|
328
337
|
${s.detail}`:`*Install failed*
|
|
329
|
-
${s.detail}`)}if(o==="uninstall"){if(!e.serviceInstallFromChat)return
|
|
330
|
-
${s.detail}`)}return
|
|
331
|
-
|
|
338
|
+
${s.detail}`)}if(o==="uninstall"){if(!e.serviceInstallFromChat)return m("Uninstall from chat is disabled. Enable with `/config set serviceInstallFromChat true` or remove files on the host manually.");let s=Os();return m(`*Uninstall*
|
|
339
|
+
${s.detail}`)}return m("Unknown /service command. Try /service help")}function Uo(e,t){let n=e.trim(),o=`/${t}`;return n===o||n.startsWith(`${o} `)?n.slice(o.length).trim():null}var _f=e=>Uo(e,"dl"),Wf=e=>Uo(e,"dlf"),Df=e=>Uo(e,"dlv"),Uf=e=>Uo(e,"tr"),Hf=e=>Uo(e,"edit"),Bf=e=>Uo(e,"pull");function Er(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 DS(e,t,n,o){let r=zt();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 US(e,t,n){return bs({peerKey:t,enabled:e.progressUpdates,sendToPeer:n?.sendToPeer})}function Ho(e,t,n){let o=se(n.peerKey).cwd,r=DS(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});if(n.friendlyReply)return{kind:"text",body:m(`${n.friendlyReply}
|
|
340
|
+
/log ${i.name??s}`)};let a=n.notify?`
|
|
341
|
+
Notify on completion: on`:"";return{kind:"text",body:m(`${n.label} job ${s} started.
|
|
332
342
|
/log ${i.name??s}
|
|
333
|
-
/tail ${i.name??s}${a}`)}}async function
|
|
343
|
+
/tail ${i.name??s}${a}`)}}async function HS(e,t,n,o){if(!e.mediaInstallFromChat)return{kind:"text",body:m("Install from chat is off. Host: omnish pull install \xB7 or /config set mediaInstallFromChat true")};let r=/\b--whisper\b/i.test(t),s=/\b--transcribe-node\b/i.test(t),i=US(e,n,o);try{let a=await Rs({whisper:r,transcribeNode:s,progress:{stepStart:(l,c,u)=>i.stepStart(l,c,{label:u})}});return{kind:"text",body:m(`*Install*
|
|
334
344
|
|
|
335
|
-
${
|
|
336
|
-
`)}`)}}catch(
|
|
345
|
+
${a.messages.join(`
|
|
346
|
+
`)}`)}}catch(a){return{kind:"text",body:m(`Install failed: ${String(a)}`)}}}async function Do(e,t,n,o,r){let s=t.trim(),i=(s.split(/\s+/)[0]??"").toLowerCase();if(!s||i==="help")return{kind:"text",body:Q(tr(e))};if(i==="doctor"){let{text:c}=Cs(e);return{kind:"text",body:m(`*Media tools*
|
|
337
347
|
|
|
338
|
-
${c}`)}}if(i==="setup"||i==="instructions")return{kind:"text",body:
|
|
348
|
+
${c}`)}}if(i==="setup"||i==="instructions")return{kind:"text",body:m(ws())};if(i==="install")return HS(e,s,n,r);let{body:a,notify:l}=Er(s);return a?Ho(e,o,{sub:"dl",body:a,peerKey:n,jobName:"dl",label:"Download",notify:l}):{kind:"text",body:Q(tr(e))}}async function jf(e,t,n,o,r){let{body:s,notify:i}=Er(t.trim());return s?Ho(e,o,{sub:"dlf",body:s,peerKey:n,jobName:"dlf",label:"File download",notify:i}):{kind:"text",body:Q(tr(e))}}async function Gf(e,t,n,o,r){let{body:s,notify:i}=Er(t.trim());return s?Ho(e,o,{sub:"dlv",body:s,peerKey:n,jobName:"dlv",label:"Video download",notify:i}):{kind:"text",body:Q(tr(e))}}async function ic(e,t,n,o,r){let s=t.trim(),i=(s.split(/\s+/)[0]??"").toLowerCase();if(!s||i==="help")return{kind:"text",body:m(`*Transcribe (/tr)*
|
|
339
349
|
|
|
340
350
|
/tr <url|path>
|
|
341
351
|
|
|
342
352
|
URL: downloads video, transcribes, sends text + .srt + video.
|
|
343
353
|
Path: transcribes local file, sends text + .srt.
|
|
344
354
|
|
|
345
|
-
Runs in background. Use /tr --notify for a completion ping when the shell job ends.`)};if(i==="doctor"||i==="setup"||i==="install")return
|
|
346
|
-
`))};if(i==="doctor"||i==="setup"||i==="install")return
|
|
355
|
+
Runs in background. Use /tr --notify for a completion ping when the shell job ends.`)};if(i==="doctor"||i==="setup"||i==="install")return Do(e,s,n,o,r);let{body:a,notify:l}=Er(s);if(!a)return{kind:"text",body:m("Usage: /tr <url|filepath>")};let c=tt(e),u=Is(e,c);return u?{kind:"text",body:m(u)}:Ho(e,o,{sub:"tr",body:a,peerKey:n,jobName:"tr",label:"Transcribe",notify:l})}async function zf(e,t,n,o,r){let s=t.trim(),i=(s.split(/\s+/)[0]??"").toLowerCase();if(!s||i==="help")return{kind:"text",body:m(["*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(`
|
|
356
|
+
`))};if(i==="doctor"||i==="setup"||i==="install")return Do(e,s,n,o,r);let{body:a,notify:l}=Er(s);return a?Ho(e,o,{sub:"edit",body:a,peerKey:n,jobName:"edit",label:"Edit",notify:l}):{kind:"text",body:m("Usage: /edit <url|path> [flags]")}}async function Jf(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&&Ts(s)?Do(e,a||s,n,o,r):i==="transcript"||i==="subs"?ic(e,a,n,o,r):i==="doctor"||i==="setup"||i==="install"||i==="help"||!s?Do(e,s,n,o,r):{kind:"text",body:m(`/pull is deprecated. Use:
|
|
347
357
|
/dl <url> \u2014 download video
|
|
348
358
|
/tr <url|path> \u2014 transcribe
|
|
349
359
|
/edit <url|path> \u2014 trim/convert
|
|
350
360
|
|
|
351
|
-
/dl help for full usage.`)}}async function
|
|
352
|
-
`))}function
|
|
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: ${
|
|
354
|
-
`))}function
|
|
355
|
-
`))}var
|
|
356
|
-
`))}function
|
|
357
|
-
`))}let a=o.match(/^show\s+(\S+)\s*$/i);if(a){let
|
|
358
|
-
`))}if(/^add$/i.test(i)){let
|
|
359
|
-
`))}let g=
|
|
360
|
-
`))}let l=o.match(/^run\s+(\S+)\s*$/i);if(l){let
|
|
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
|
|
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
|
|
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
|
|
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
|
|
365
|
-
`))}function
|
|
366
|
-
`))}function
|
|
367
|
-
`))}function
|
|
368
|
-
`))}function
|
|
369
|
-
`)))}if(/^reload$/i.test(o)){let{summary:u}=
|
|
370
|
-
`)))}let r=n.match(/^recent(?:\s+(\d+))?\s*$/i);if(r){let u=r[1]?Number(r[1]):15,d=Ue(),
|
|
371
|
-
`)))}let s=n.match(/^show\s+(\S+)\s*$/i);if(s){let u=
|
|
372
|
-
`)))}if(/^on$/i.test(o))return
|
|
373
|
-
`):"(none)"}`,`excludeGlobs: ${
|
|
374
|
-
`):"(none)"}`];return
|
|
375
|
-
`)))}if(d==="add"){if(!
|
|
376
|
-
`))}async function
|
|
377
|
-
`))}if(!t.tunnelEnabled)return
|
|
361
|
+
/dl help for full usage.`)}}async function qf(e,t,n,o,r){if(!(e.mediaUrlAutoDl||!e.chatLlmFallbackEnabled&&!e.chatLlmShellCommand.trim()))return null;let i=tl(t);return i?Ho(e,o,{sub:"dl",body:i,peerKey:n,jobName:"dl",label:"Download",notify:!1,friendlyReply:"Downloading link\u2026 I'll send the file when it's ready."}):null}ce();At();$n();function Li(){let e=he();return e?.platformUrl?e.platformUrl.replace(/\/$/,""):(S().tunnelRelayUrl||Ee).trim().replace(/\/$/,"")}function Oi(){let e=he();return e?.token?{Authorization:`Bearer ${e.token}`}:{}}async function Ar(e){try{return await e.json()}catch{return{error:`HTTP ${e.status}`}}}function BS(){return`Set platform URL: omnish config add tunnelRelayUrl ${Ee} \u2014 publish: omnish platform login`}function ac(){return he()?.token?{ok:!0}:{ok:!1,error:`Publish requires a platform account token. ${BS()}`}}async function Kf(e){let t=new URLSearchParams;e?.kind&&t.set("kind",e.kind),e?.limit&&t.set("limit",String(e.limit));let n=t.toString(),o=`${Li()}/v1/catalog/trending${n?`?${n}`:""}`,r=await fetch(o,{headers:Oi()}),s=await Ar(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}async function Yf(e,t){let n=new URLSearchParams;n.set("q",e),t?.kind&&n.set("kind",t.kind),t?.category&&n.set("category",t.category),t?.limit&&n.set("limit",String(t.limit));let o=`${Li()}/v1/catalog/search?${n}`,r=await fetch(o,{headers:Oi()}),s=await Ar(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}async function Qf(e){let t=encodeURIComponent(e.trim()),n=`${Li()}/v1/catalog/${t}`,o=await fetch(n,{headers:Oi()}),r=await Ar(o);return o.ok?r:{error:r.error??`HTTP ${o.status}`}}async function Vf(e){let t=encodeURIComponent(e.trim()),n=`${Li()}/v1/catalog/${t}/download`,o=await fetch(n,{method:"POST",headers:Oi()}),r=await Ar(o);return o.ok?r:{error:r.error??`HTTP ${o.status}`}}async function Xf(e){let t=ac();if(!t.ok)return{error:t.error};let n=he(),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 Ar(r);return r.ok?s:{error:s.error??`HTTP ${r.status}`}}function Zf(e){return e==="global"?"global":"chat"}function eh(e,t,n,o="global"){switch(e.kind){case"recipe":{let r=e.payload,s=Xe({...r,dangerous:void 0}),i=_n(s);if(!i.ok)return{ok:!1,error:i.error};let a=Et(e.name);if(!a.ok)return{ok:!1,error:a.error};try{xo(t,a.normalized,s,Zf(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=Xe({command:r.command.trim(),label:r.label??e.title,description:r.description??e.description,category:e.category||"app"}),i=Et(e.name);if(!i.ok)return{ok:!1,error:i.error};try{xo(t,i.normalized,s,Zf(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=bt(e.name);if(!s.ok)return{ok:!1,error:s.error};try{Pr(t,s.normalized,r.body,o==="global"?"global":"chat")}catch(i){return{ok:!1,error:String(i.message??i)}}return{ok:!0,message:`Installed shortcut "${s.normalized}" (${o}). Type ${s.normalized} to expand.`}}case"cowork":{let r=e.payload,s=Pe();if(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:Vo(),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 th(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 nh(e){let{dangerous:t,...n}=e;return Xe(n)}var lc=new Map;function cc(e,t){return`${e}:${t}`}function uc(e,t,n){lc.set(cc(e,t),n)}function oh(e,t){return lc.get(cc(e,t))}function Ni(e,t,n){let o=Number.parseInt(n,10);if(!Number.isFinite(o)||o<1)return null;let r=lc.get(cc(e,t));return!r||o>r.length?null:r[o-1].publicId}function Jn(e){return!!(e&&typeof e=="object"&&typeof e.error=="string")}function Fi(e,t){return`${e.commandPrefix} online ${t}`}function rh(e){let t=r=>Fi(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"),m(o.join(`
|
|
362
|
+
`))}function _i(e,t,n){if(e.length===0)return m(`${t}
|
|
363
|
+
(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: ${Fi(n,"<publicId> download")} \u2014 or ${Fi(n,"trending <n> download")}`),m(o.join(`
|
|
364
|
+
`))}function sh(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("",Fi(t,`${e.publicId} download`)),m(n.join(`
|
|
365
|
+
`))}var dc=new Set(["recipe","app","cowork","shortcut"]),jS={commandPrefix:"/run",listScope:"run"},ih={commandPrefix:"/apps",defaultKind:"app",listScope:"apps"},ah={commandPrefix:"/cowork",defaultKind:"cowork",listScope:"cowork"},lh={commandPrefix:"/shortcut",defaultKind:"shortcut",listScope:"shortcut"};function GS(e){if(!e)return;let t=e.toLowerCase();return dc.has(t)?t:void 0}function Qt(e,t){return`${e.commandPrefix} online ${t}`}function zS(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=GS(t);return t&&!n?{error:`Unknown kind "${t}". Use: recipe, app, cowork, shortcut`}:n}function JS(e,t){if(e.defaultKind){if(t.length>1&&dc.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&&dc.has(t[t.length-1].toLowerCase())&&(n=t.pop().toLowerCase()),{kind:n,query:t.join(" ")}}async function Bo(e,t,n,o){let r=e.trim();if(!r||/^help$/i.test(r))return rh(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",p=null,f=/^trending\s+(\d+)$/i.exec(c),h=/^search\s+(\S+)\s+(\d+)$/i.exec(c);if(f){if(p=Ni(t,o.listScope,f[1]),!p)return m(`No trending list in this chat \u2014 run ${Qt(o,"trending")} first, or use ${Qt(o,"<publicId> download")}.`)}else if(h){if(p=Ni(t,o.listScope,h[2]),!p)return m(`No search list in this chat \u2014 run ${Qt(o,"search <query>")} first, or use ${Qt(o,"<publicId> download")}.`)}else if(/^\d+$/.test(c)){if(p=Ni(t,o.listScope,c),!p)return m(`No list item #${c}. Run trending or search first, or use ${Qt(o,"<publicId> download")}.`)}else p=c;let g=await Vf(p);if(Jn(g))return m(`Download failed: ${g.error}`);if(o.defaultKind&&g.kind!==o.defaultKind)return m(`Entry "${p}" is kind=${g.kind}, not ${o.defaultKind}. Use /run online show ${p} from /run.`);let y=eh(g,t,n,d);return y.ok?m(y.message):m(`Download failed: ${y.error}`)}let i=/^show\s+(\S+)\s*$/i.exec(r);if(i){let c=await Qf(i[1]);return Jn(c)?m(`Not found: ${c.error}`):o.defaultKind&&c.kind!==o.defaultKind?m(`Entry "${i[1]}" is kind=${c.kind}, not ${o.defaultKind}. Use /run online show ${i[1]}.`):sh(c,o)}let a=/^trending(?:\s+(\S+))?\s*$/i.exec(r);if(a){let c=zS(o,a[1]);if(typeof c=="object"&&"error"in c)return m(c.error);let u=await Kf(c?{kind:c}:void 0);if(Jn(u))return m(`Trending failed: ${u.error}`);uc(t,o.listScope,u.items);let d=o.defaultKind?`Trending (${o.defaultKind})`:"Trending";return _i(u.items,d,o)}let l=/^search\s+([\s\S]+)$/i.exec(r);if(l){let c=l[1].trim().split(/\s+/),u=JS(o,c);if("error"in u)return m(u.error);let{kind:d,query:p}=u;if(!p)return m(`Usage: ${Qt(o,"search <query>")}`);let f=await Yf(p,d?{kind:d}:void 0);return Jn(f)?m(`Search failed: ${f.error}`):(uc(t,o.listScope,f.items),_i(f.items,`Search: ${p}`,o))}if(/^list\s*$/i.test(r)){let c=oh(t,o.listScope);return c?.length?_i(c,"Last list",o):m(`No cached list. ${Qt(o,"trending")} or ${Qt(o,"search <query>")}`)}return m(`Unknown ${o.commandPrefix} online command. ${Qt(o,"help")}`)}async function ch(e,t,n){return Bo(e,t,n,jS)}function Ir(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 qS(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:nh(t)}}function uh(e,t,n,o){let r=Ir(o),s=n.trim().toLowerCase(),i=Ze(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:qS(i.name,i,r)}:{ok:!1,error:`Unknown recipe "${n}".`}}function dh(e,t,n){let o=Ir(n),r=Ii(e,t);return r?{ok:!0,body:{kind:"shortcut",name:r.name,title:o.title??r.name,description:o.description,category:o.category??"shortcut",tags:o.tags,payload:{body:r.body}}}:{ok:!1,error:`Unknown shortcut "${t}".`}}function ph(e,t,n){let o=Ir(n),r=Pe(),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:th(s)}}:{ok:!1,error:`Unknown cowork task "${t}" for this chat.`}}function mh(e,t,n){let o=Ir(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 jo(e){let t=ac();if(!t.ok)return{ok:!1,error:t.error};let n=await Xf(e);if(Jn(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 KS(){return m(["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(`
|
|
366
|
+
`))}function fh(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 YS(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 gh(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return KS();let r=/^online\b([\s\S]*)$/i.exec(o);if(r)return Bo((r[1]??"").trim(),t,n,ah);let s=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(o);if(s){let f=ph(t,s[1],s[2]??"");if(!f.ok)return m(f.error);let h=await jo(f.body);return m(h.ok?h.message:h.error)}let i=o.split(/\s+/)[0].toLowerCase();if(/^list$/i.test(i)){let f=Pe().filter(g=>g.ownerPeerKey===t);if(f.length===0)return m("(no cowork tasks for this chat)");let h=f.map(g=>{let y=g.enabled?"":" (disabled)",b=g.notifyWhen&&g.notifyWhen!=="always"?` when=${g.notifyWhen}`:"";return`${Ae}${g.name} ${Co(g.schedule)} notify=${g.notify}${b}${y}`});return m(h.join(`
|
|
367
|
+
`))}let a=o.match(/^show\s+(\S+)\s*$/i);if(a){let f=a[1],h=Pe();Dn(h);let g=rt(h,f,t);if(!g)return m(`Unknown task "${f}". /cowork list`);let y=Qs(g),b=[`name: ${g.name}`,`id: ${g.id}`,`schedule: ${Co(g.schedule)}`,`enabled: ${g.enabled}`,`notify: ${g.notify}`,`notifyWhen: ${g.notifyWhen??"always"}`];if(g.schedule.kind==="heartbeat"){let k=Xs(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 m(b.join(`
|
|
368
|
+
`))}if(/^add$/i.test(i)){let f=o.slice(3).trim(),h=f.match(/^(\S+)\s+(heartbeat\s+.+)$/i);if(h){let C=da(h[1]);if(!C.ok)return m(C.error);let N=h[2].split(/\s+/).filter(Boolean),I=Ys(N);if(!I.ok)return m(I.error);let K=Pe();if(rt(K,C.name,t))return m(`Task "${C.name}" already exists. Remove it first or pick another name.`);let ee=Xr(C.name),le={id:Vo(),name:C.name,ownerPeerKey:t,command:"",cwd:"",outputDir:ee,schedule:I.schedule,enabled:!0,notify:"self",notifyWhen:"always",attachLog:!1,attachFiles:[],lastCompletedSlotMs:null,createdAtMs:Date.now()};return K.push(le),De(K),Dn(K),$l(le.id),m([`Saved heartbeat task "${C.name}" (${Co(I.schedule)}).`,`Send /cowork checkin ${C.name} to record a heartbeat.`,"Alerts if no check-in arrives within the expected interval + grace period."].join(`
|
|
369
|
+
`))}let g=YS(f);if("error"in g)return m(g.error);let y=da(g.name);if(!y.ok)return m(y.error);let b=Ys(g.scheduleWords);if(!b.ok)return m(b.error);let k=Pe();if(rt(k,y.name,t))return m(`Task "${y.name}" already exists. Remove it first or pick another name.`);let x=se(t),T=Xr(y.name),O={id:Vo(),name:y.name,ownerPeerKey:t,command:g.command,cwd:"",outputDir:T,schedule:b.schedule,enabled:!0,notify:"self",notifyWhen:"always",attachLog:!1,attachFiles:[],lastCompletedSlotMs:null,createdAtMs:Date.now()};return k.push(O),De(k),m([`Saved cowork task "${y.name}" (${Co(b.schedule)}).`,`Output: ${T}`,`Notify: self \u2014 change with /cowork set ${y.name} notify wa|tg|all|none`].join(`
|
|
370
|
+
`))}let l=o.match(/^run\s+(\S+)\s*$/i);if(l){let f=l[1].toLowerCase(),h=rt(Pe(),f,t);return h?h.enabled?(ru({ownerPeerKey:t,name:h.name,at:Date.now()}),m(`On-demand run queued for "${h.name}" (runs within ~30s while omnish run is active).`)):m(`Cowork "${h.name}" is disabled. /cowork enable ${h.name}`):m(`Unknown task "${l[1]}". /cowork list`)}let c=o.match(/^checkin\s+(\S+)\s*$/i);if(c){let f=c[1].toLowerCase(),h=Pe();Dn(h);let g=rt(h,f,t);return g?g.schedule.kind!=="heartbeat"?m(`"${g.name}" is not a heartbeat task.`):($l(g.id),m(`Heartbeat recorded for "${g.name}".`)):m(`Unknown task "${c[1]}". /cowork list`)}let u=o.match(/^(?:remove|rm|del)\s+(\S+)\s*$/i);if(u){let f=u[1],h=Pe(),g=h.findIndex(y=>y.name===f.toLowerCase()&&y.ownerPeerKey===t);return g===-1?m(`Unknown task "${f}".`):(h.splice(g,1),De(h),m(`Removed cowork task "${f.toLowerCase()}".`))}let d=o.match(/^enable\s+(\S+)\s*$/i);if(d)return hh(d[1],t,!0);let p=o.match(/^disable\s+(\S+)\s*$/i);return p?hh(p[1],t,!1):/^set$/i.test(i)?ZS(o.slice(3).trim(),t):m("Unknown /cowork command. Try /cowork help")}function hh(e,t,n){let o=Pe(),r=rt(o,e,t);return r?(r.enabled=n,De(o),m(`Cowork "${r.name}" ${n?"enabled":"disabled"}.`)):m(`Unknown task "${e}".`)}function QS(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 VS(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 XS(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 ZS(e,t){let n=e.match(/^(\S+)\s+([\s\S]+)$/);if(!n)return m("Usage: /cowork set <name> cmd -- \u2026 | schedule \u2026 | out <path> | cwd <path> | notify \u2026 | when \u2026 | attach <on|off> | files \u2026");let o=n[1],r=n[2].trim(),s=Pe(),i=rt(s,o,t);if(!i)return m(`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 m("Usage: /cowork set <name> cmd -- <command\u2026>");return u?(i.command=u,De(s),m(`Updated command for "${i.name}".`)):m("Command is empty.")}if(r.toLowerCase().startsWith("schedule ")){let a=r.slice(9).trim().split(/\s+/).filter(Boolean),l=Ys(a);return l.ok?(i.schedule=l.schedule,De(s),m(`Schedule for "${i.name}": ${Co(l.schedule)}`)):m(l.error)}if(r.toLowerCase().startsWith("out ")){let a=r.slice(4).trim(),l=se(t);return i.outputDir=et(a,l.cwd),De(s),m(`outputDir: ${i.outputDir}`)}if(r.toLowerCase().startsWith("cwd ")){let a=r.slice(4).trim(),l=se(t);return i.cwd=a?et(a,l.cwd):"",De(s),m(`cwd: ${i.cwd||"(session cwd at run time)"}`)}if(r.toLowerCase().startsWith("notify ")){let a=r.slice(7).trim(),l=QS(a);return l?(i.notify=l,De(s),m(`notify: ${l}`)):m("notify must be self, wa, tg, all, or none.")}if(r.toLowerCase().startsWith("when ")){let a=r.slice(5).trim(),l=VS(a);return l?(i.notifyWhen=l,De(s),m(`notifyWhen: ${l}`)):m("when must be always, failure, or state-change.")}if(r.toLowerCase().startsWith("attach ")){let a=r.slice(7).trim(),l=fh(a);if(l.length!==1)return m("Usage: /cowork set <name> attach on|off");let c=XS(l[0]);return c===null?m("attach must be on or off"):(i.attachLog=c,De(s),m(`attachLog: ${c}`))}if(r.toLowerCase().startsWith("files ")){let a=r.slice(6).trim(),l=fh(a);return l.length===1&&l[0].toLowerCase()==="clear"?(i.attachFiles=[],De(s),m("files: (cleared)")):l.length===0?m("Usage: /cowork set <name> files clear | <pattern \u2026> \u2014 quote paths with spaces"):(i.attachFiles=l,De(s),m(`files: ${i.attachFiles.join(", ")}`))}return m("Unknown set field. Try: cmd, schedule, out, cwd, notify, when, attach, files")}ce();import kx from"node:fs";j();import yh from"node:os";import ex from"node:path";var tx=new Set(["create","delete","rename","update"]);function nx(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 ox(e){let t=e.split(",").map(n=>n.trim().toLowerCase());return t.length===0?!1:t.every(n=>tx.has(n))}function rx(e){return e.split(",").map(t=>t.trim().toLowerCase())}function pc(e,t,n){let o=e.trim();if(o.startsWith("-")&&(o=o.slice(1)),!o)return{};let r=et(o,n);return o.includes("*")||o.includes("?")?{glob:o.replace(/\\/g,"/")}:ex.isAbsolute(r)||o.startsWith("~")||o.startsWith("./")||o.startsWith("../")?{path:r}:o.startsWith("/")?{path:r}:{glob:o.includes("/")?o:`**/${o}`}}function wh(e,t=yh.homedir()){let n=e.replace(/\s+&&\s+/g," ").trim(),o=nx(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 p=et(s,t),f=pc(d.startsWith("-")?d:`-${d}`,p,t);f.path?a.push(f.path):f.glob&&l.push(f.glob);continue}if(u.startsWith("--"))return{ok:!1,error:`Unknown flag: ${u}`};if(u.startsWith("-")){let d=et(s,t),p=pc(u,d,t);p.path?a.push(p.path):p.glob&&l.push(p.glob),r+=1;continue}if(ox(u)){i=rx(u),r+=1;continue}return{ok:!1,error:`Unexpected token "${u}". Put path first, then events or -excludes.`}}return{ok:!0,value:{rootPath:et(s,t),events:i,excludePaths:a,excludeGlobs:l}}}function mc(e,t,n=yh.homedir()){let o=pc(e.startsWith("-")?e:`-${e}`,t,n);return!o.path&&!o.glob?{error:"Empty exclude pattern."}:o}import{spawnSync as Wi}from"node:child_process";import bh from"node:fs";import sx from"node:os";import ix from"node:path";var ax=40,lx=10,Di=15e3,cx=["~/Projects","~/deploy","~/Downloads","~/src","/var/www","/srv"],kh={linux:"/var/log/dpkg.log (also /var/log/apt/history.log)",darwin:"/var/log/install.log",win32:"Windows Application event log (install/remove)"};function ux(e){return e.startsWith("~/")?ix.join(sx.homedir(),e.slice(2)):e}function dx(e,t){return t?.trim()?e.toLowerCase().includes(t.trim().toLowerCase()):!0}function px(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 mx(e){let t=[];for(let n of e.split(`
|
|
371
|
+
`)){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 fx(e){let t=[],n=e.split(`
|
|
372
|
+
`);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 hx(e){let t=[],n="";for(let o of e.split(`
|
|
373
|
+
`)){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 gx(){let e=Wi("systemctl",["list-units","--type=service","--all","--no-pager","--plain","--no-legend"],{encoding:"utf8",timeout:Di});return e.error||e.status!==0?{entries:[],error:"systemctl unavailable \u2014 install systemd or run on Linux."}:{entries:mx(e.stdout??"")}}function yx(){let e=Wi("launchctl",["list"],{encoding:"utf8",timeout:Di});return e.error||e.status!==0?{entries:[],error:"launchctl unavailable."}:{entries:fx(e.stdout??"")}}function wx(){let e=Wi("sc",["query","type=","service","state=","all"],{encoding:"utf8",timeout:Di,windowsHide:!0});if(!e.error&&e.status===0&&(e.stdout??"").includes("SERVICE_NAME"))return{entries:hx(e.stdout??"")};let t=Wi("powershell",["-NoProfile","-Command","Get-Service | ForEach-Object { $_.Name + ' ' + $_.Status }"],{encoding:"utf8",timeout:Di,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(`
|
|
374
|
+
`)){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 Ui(e){let t=e?.limit??ax,n=e?.filter,o,r=process.platform;if(r==="linux")o=gx();else if(r==="darwin")o=yx();else if(r==="win32")o=wx();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=px(o.entries.filter(l=>dx(l.name,n))),i=s.length,a=i>t;return{entries:s.slice(0,t),truncated:a,totalMatched:i}}function bx(e,t=lx){let n=[];for(let o=0;o<e.length;o+=t)n.push(e.slice(o,o+t));return n}function fc(e,t){if(e.error)return m(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 m(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."),m(n.join(`
|
|
375
|
+
`))}function hc(e,t){let n=t?.trim()||"<name>";if(e.length===0)return m("No services to template \u2014 run /watch svc list first.");let o=e.map(i=>i.name),r=bx(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 m(s.join(`
|
|
376
|
+
`))}function vh(){let e=process.platform,t=e==="linux"?"linux":e==="darwin"?"darwin":"win32",n=["Watch hints",""];n.push("Package log (pkg watches):"),n.push(` ${kh[t]??kh.linux}`),n.push("","Filesystem roots that exist on this host:");let o=[];for(let r of cx){let s=ux(r);try{bh.existsSync(s)&&bh.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]"),m(n.join(`
|
|
377
|
+
`))}function B(e){return{replies:[e]}}function vx(...e){return{replies:e}}function Sx(e){let t=e.trim();if(!t)return{};let n=t.split(/\s+/);if(n.length>=2){let s=ts(n[0]);return s.ok?{ruleName:s.name,filter:n.slice(1).join(" ")}:{filter:t}}let o=n[0],r=ts(o);if(r.ok){let s=Ui({filter:o,limit:1});if(s.totalMatched===0&&!s.error)return{ruleName:r.name}}return{filter:o}}function xx(){return m(["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(`
|
|
378
|
+
`))}function Sh(){return{excludePaths:[],excludeGlobs:[]}}function Cx(e){return e.notify==="self"?`notify=self (${e.ownerPeerKey})`:`notify=${e.notify}`}function Rx(e,t){ga(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 xh(e,t){let n=e.trim();if(!n||/^help$/i.test(n))return B(xx());let o=n.split(/\s+/)[0].toLowerCase();if(Zr(),/^hints$/i.test(o))return B(vh());if(/^svc$/i.test(o)){let u=n.slice(3).trim(),p=(u.split(/\s+/)[0]??"").toLowerCase();if(p==="list"){let f=u.slice(4).trim()||void 0,h=Ui({filter:f});return h.error?B(m(h.error)):h.entries.length===0?B(fc(h,f)):vx(fc(h,f),hc(h.entries))}if(p==="templates"){let f=u.slice(9).trim(),{ruleName:h,filter:g}=Sx(f),y=Ui({filter:g});return y.error?B(m(y.error)):B(hc(y.entries,h))}return B(m("svc subcommands: list [filter] | templates [ruleName] [filter]"))}if(/^status$/i.test(o)){let u=S(),{summary:d}=Zo(),p=[`watchEnabled: ${u.watchEnabled} watchAutoRestore: ${u.watchAutoRestore}`,`debounce: ${u.watchDebounceMs}ms max/min: ${u.watchMaxEventsPerMinute}`,`rules file: ${fa()} (${d.total} saved, ${d.active} active, ${d.paused} paused, ${d.disabled} disabled)`,`events db: ${jr}`];d.total>0&&!u.watchEnabled?p.push("Rules are on disk; adapters stopped \u2014 /watch on to start."):d.total>0&&u.watchEnabled&&!u.watchAutoRestore?p.push("watchAutoRestore is off \u2014 /watch reload to start adapters without restarting gateway."):d.paused>0&&p.push(`${d.paused} paused rule(s) stay stopped until /watch resume <name>.`);let f=Lu();return f?(p.push("","Adapters:"),p.push(...f.getStatusLines())):u.watchEnabled&&u.watchAutoRestore?p.push("","(manager starting \u2014 try /watch status again)"):u.watchEnabled?p.push("","(manager idle \u2014 /watch reload)"):p.push("","(manager off \u2014 /watch on)"),B(m(p.join(`
|
|
379
|
+
`)))}if(/^reload$/i.test(o)){let{summary:u}=Zo();return S().watchEnabled?(Ou(),B(m(`Reloading from ${fa()}: ${u.total} rule(s) on disk, ${u.active} eligible to run. /watch status for adapters.`))):B(m(`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 B(m("(no watch rules on this device)"));let d=u.map(p=>{let f=p.enabled?p.paused?"paused":"on":"disabled",h=p.kind==="fs"?p.path:p.kind==="svc"?p.units.join(","):"system";return`${Ae}${p.name} [${p.kind}] ${f} ${Cx(p)} when=${p.notifyWhen} \u2014 ${h}`});return B(m(d.join(`
|
|
380
|
+
`)))}let r=n.match(/^recent(?:\s+(\d+))?\s*$/i);if(r){let u=r[1]?Number(r[1]):15,d=Ue(),p=new Set(d.map(g=>g.id)),f=uu(u,p);if(f.length===0)return B(m("(no recent watch events)"));let h=f.map(g=>`${new Date(g.tsMs).toLocaleString()} ${g.ruleName} ${g.summary}`);return B(m(h.join(`
|
|
381
|
+
`)))}let s=n.match(/^show\s+(\S+)\s*$/i);if(s){let u=Cn(Ue(),s[1]);if(!u)return B(m(`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)"}`),B(m(d.join(`
|
|
382
|
+
`)))}if(/^on$/i.test(o))return A({watchEnabled:!0}),Ye(),B(m("watchEnabled: true (gateway picks up watchers when running)."));if(/^off$/i.test(o))return A({watchEnabled:!1}),Ye(),B(m("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(),p=(i[3]??"").trim(),f=Ue(),h=Cn(f,u);if(!h)return B(m(`Unknown rule "${u}".`));if(h.kind!=="fs")return B(m("exclude applies to filesystem rules only."));if(d==="list"){let g=[`excludePaths: ${h.excludePaths.length?h.excludePaths.join(`
|
|
383
|
+
`):"(none)"}`,`excludeGlobs: ${h.excludeGlobs.length?h.excludeGlobs.join(`
|
|
384
|
+
`):"(none)"}`];return B(m(g.join(`
|
|
385
|
+
`)))}if(d==="add"){if(!p)return B(m("Usage: /watch exclude <name> add <pattern>"));let g=mc(p,h.path);if("error"in g)return B(m(g.error));if(g.path){if(st(g.path))return B(m("That path is blocked."));h.excludePaths.includes(g.path)||h.excludePaths.push(g.path)}return g.glob&&!h.excludeGlobs.includes(g.glob)&&h.excludeGlobs.push(g.glob),Ct(so(f,h)),Ye(),B(m(`Added exclude to "${h.name}".`))}if(d==="rm"||d==="remove"){if(!p)return B(m("Usage: /watch exclude <name> rm <pattern>"));let g=mc(p,h.path);return"error"in g?B(m(g.error)):(g.path&&(h.excludePaths=h.excludePaths.filter(y=>y!==g.path)),g.glob&&(h.excludeGlobs=h.excludeGlobs.filter(y=>y!==g.glob)),Ct(so(f,h)),Ye(),B(m(`Removed exclude from "${h.name}".`)))}return B(m("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 B(m("Usage: /watch add fs|pkg|svc <name> \u2026"));let p=d[0].toLowerCase(),f=p==="fs"||p==="pkg"||p==="svc"?p:null;if(!f)return B(m("Kind must be fs, pkg, or svc."));let h=d[1],g=ts(h);if(!g.ok)return B(m(g.error));let y=Ue();if(y.length>=ma)return B(m(`Max ${ma} watch rules on this device.`));if(Cn(y,g.name))return B(m(`Rule "${g.name}" already exists.`));let b;if(f==="fs"){let T=u.slice(p.length+1+h.length).trim(),O=wh(T);if(!O.ok)return B(m(O.error));let{rootPath:C,events:N,excludePaths:I,excludeGlobs:K}=O.value;if(st(C))return B(m("That path is blocked (sensitive). Choose another directory."));if(!kx.existsSync(C))return B(m(`Path not found: ${C}`));for(let ee of I)if(st(ee))return B(m(`Excluded path blocked: ${ee}`));b={id:ns(),name:g.name,ownerPeerKey:t,kind:"fs",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:C,events:N,units:[],excludePaths:I,excludeGlobs:K,adapterStatus:"",createdAtMs:Date.now()}}else if(f==="pkg")b={id:ns(),name:g.name,ownerPeerKey:t,kind:"pkg",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:"",events:[],units:[],...Sh(),adapterStatus:"",createdAtMs:Date.now()};else{let T=d.slice(2);if(T.length===0)return B(m("Usage: /watch add svc <name> <unit\u2026>"));b={id:ns(),name:g.name,ownerPeerKey:t,kind:"svc",enabled:!0,paused:!1,notify:"self",notifyWhen:"always",path:"",events:[],units:T,...Sh(),adapterStatus:"",createdAtMs:Date.now()}}Ct(so(y,b)),Ye();let x=S().watchEnabled?"":" Rule saved to disk. /watch on to start adapters.";return B(m(`Watch rule "${b.name}" added (${b.kind}).${x}`))}let a=n.match(/^set\s+(\S+)\s+(\S+)\s+(\S+)\s*$/i);if(a){let[,u,d,p]=a,f=Ue(),h=Cn(f,u);if(!h)return B(m(`Unknown rule "${u}".`));let g=d.toLowerCase(),y=p.toLowerCase();if(g==="notify"){let b=y==="wa"||y==="whatsapp"?"wa":y==="tg"||y==="telegram"?"tg":y==="all"?"all":y==="none"?"none":y==="self"?"self":null;if(!b)return B(m("notify must be self, wa, tg, all, or none."));h.notify=b}else if(g==="when"){let b=y==="state-change"?"state-change":y==="always"?"always":null;if(!b)return B(m("when must be always or state-change."));h.notifyWhen=b}else return B(m("set supports: notify, when"));return Ct(so(f,h)),Ye(),B(m(`Updated ${h.name} ${g}=${y}.`))}let l=n.match(/^(?:rm|remove)\s+(\S+)\s*$/i);if(l){let u=l[1],d=Ue(),p=Cn(d,u);return p?(ga(p.id),Ct(gu(d,u)),Ye(),B(m(`Removed watch rule "${u}".`))):B(m(`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],p=Ue(),f=Cn(p,d);if(!f)return B(m(`Unknown rule "${d}".`));Rx(f,u==="stop"?"pause":u),Ct(so(p,f)),Ye();let g=u==="stop"?"paused (stop)":`${u}d`;return B(m(`${f.name}: ${g}.`))}return B(m("Unknown /watch command. Try /watch help"))}ce();Tn();$n();function Ch(){return m(["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(`
|
|
386
|
+
`))}async function gc(e,t,n){let o=sf(e);if(o.kind==="help")return Ch();if(o.kind==="error")return m(o.message);if(o.kind==="signup"){let r=o.email?.trim()||"",s=o.phone?.trim()||"",i=o.password||"";if(!r&&!s)return m('Usage: /tunnel signup --email "you@example.com" --password "yourpass"');if(i.length<8)return m("Password must be at least 8 characters.");let a=S(),l=o.relayUrl?.trim()||mt(a.tunnelRelayUrl||Ee),c=await Ci(l,{...r?{email:r}:{},...s?{phone:s}:{},password:i});return c.ok?(Tt({token:c.token,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),m("Account created. Token saved.")):m(`Signup failed: ${c.error}`)}if(o.kind==="login"){if(o.email||o.phone){let i=o.password||"";if(!i)return m('Usage: /tunnel login --email "you@example.com" --password "yourpass"');let a=S(),l=o.relayUrl?.trim()||mt(a.tunnelRelayUrl||Ee),c=await Ri(l,{...o.email?{email:o.email}:{},...o.phone?{phone:o.phone}:{},password:i});return c.ok?(Tt({token:c.token,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),m("Logged in. Token saved.")):m(`Login failed: ${c.error}`)}let s=o.token?.trim();return s?(Tt({token:s,...o.relayUrl?.trim()?{relayUrl:o.relayUrl.trim()}:{}}),m("Tunnel token saved on this host (not shown). If OMNISH_TUNNEL_TOKEN is set in the gateway environment, it overrides the file until unset.")):m('Usage: /tunnel login --token "<token>" [--relay <url>]')}if(o.kind==="logout")return ps(),m("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=$t(),a=!!process.env.OMNISH_TUNNEL_TOKEN?.trim(),c=!!Dt()?.token?.trim(),u=await ki(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}`),m(d.join(`
|
|
387
|
+
`))}if(!t.tunnelEnabled)return m("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?m("(no active tunnels)"):m(r.map(s=>`${s.id} ${s.kind} ${s.status}
|
|
378
388
|
${s.publicUrl||"(pending)"}
|
|
379
389
|
${s.localHost}:${s.localPort}`).join(`
|
|
380
390
|
|
|
381
|
-
`))}if(o.kind==="stop"){let r=await n.stop(o.target);return r?
|
|
391
|
+
`))}if(o.kind==="stop"){let r=await n.stop(o.target);return r?m(`Stopped tunnel ${r.id}.`):m(`No active tunnel matched "${o.target}".`)}if(o.kind==="expose"){let r=await n.expose(t,o.options);return m(`${r.kind.toUpperCase()} tunnel active
|
|
382
392
|
public: ${r.publicUrl}
|
|
383
393
|
local: ${r.localHost}:${r.localPort}
|
|
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(`
|
|
394
|
+
id: ${r.id}`)}return Ch()}var Rh={version:1,generatedAt:"2026-05-24T19:35:09.102Z",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) Lone-URL auto-download ( or LLM off \u2192 background ) Free Shell Mode (when enabled) Chat LLM fallback (optional plain-text handler when configured) 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 After stripping the sync prefix ( by default) or when an unknown slash is not a built-in command, omnish calls : only \u2014 expand the stored body (legacy bare shortcuts). \u2014 if the body contains , substitute trimmed input for every placeholder; if the placeholder is present but input is missing, reply with a usage error. Bodies without ignore extra words after the name. Expansion re-enters once with (no nested shortcut expansion). 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","/dl","/dl job","/disable direct","/name","/new","/path","/router","/config","/config set","/computers","/pcs"]},{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. Share-friendly filenames All download paths normalize saved basenames for easy sharing in chat: ASCII-safe names (unicode stripped or transliterated, emoji removed) Spaces and unsafe characters become Collisions get numeric suffixes ( , ) yt-dlp uses plus a post-download rename pass Whisper sidecars ( , , etc.) inherit the normalized video basename. 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. When chat LLM fallback is off and no LLM shell command is configured, lone URLs also auto-download even if was set to false (paste-a-link intent). Trailing punctuation from chat clients (e.g. ) is stripped before detection. Auto-dl replies with a short \u201CDownloading link\u2026\u201D message instead of a verbose job block. 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) Max size when sending downloads to chat (0 = no omnish cap; WA/TG limits still apply) Whisper / Transformers model size ( , , , \u2026) (default device, then CPU), , or (whisper CLI only) Per-attempt timeout for whisper/ffmpeg (10 min; max 15 min) , , or (whisper then Transformers.js fallback) When engine fails, retry with Transformers.js if installed On hosts with a busy or small GPU, set to or use a smaller ( , ). For long files that hit timeouts, raise . Transformers.js (no Python): installs under . Set to to use it exclusively, or keep / with (default) to fall back when openai-whisper fails. Step-by-step chat messages during multi-step work / / Binary overrides Older keys in are migrated on load (e.g. \u2192 ). Size limits ( , , ) Two different caps: Key When it applies ----- ----------------- Download only (yt-dlp ). = no cap. Sending the saved file to chat after the job finishes. = no omnish cap on standalone . If the file downloads but chat says \u201CN file(s) not sent\u201D with a size, the job succeeded on disk; outbound delivery hit a cap \u2014 not . Standalone (local WhatsApp/Telegram on the host): only (and messenger limits). Attached ( with platform credentials, messengers on the relay): no default platform hop cap. Optional env (bytes; = unlimited) on both the relay and the device gateway caps outbound files over the WebSocket. WhatsApp/Telegram limits still apply upstream. Check values: (or ) for and ."},{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 any cap in attached mode) 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 Parameterized shortcuts ( ) Put in the saved command where runtime text should go. Everything after the shortcut name on the same line is substituted in (trimmed; not shell-parsed). Shortcuts without still work as bare or only (extra words on the line are ignored). Shortcuts with require input; bare or returns a usage hint. Input length follows the same cap as tasks when is set. 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. ). Optional substitutes chat text inline. 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, custom wrappers with varying text. Drive system agents (e.g. , , your orchestrators) from chat with a task each time. Invocation or ; with in the body: or . \u2014 task is required for named recipes. Shortcuts substitute in the expanded line only (no shell env). Recipes require the stored command to reference (or a custom ) and run in a PTY session. 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 vn=Rh;function Hi(e,t){return`${e.replace(/\/$/,"")}/${t}`}function $x(e){return e.toLowerCase().replace(/[^a-z0-9\s/-]/g," ").split(/\s+/).filter(t=>t.length>1)}function Mx(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(),p=u.body.toLowerCase();d.includes(c)&&(i+=5,a??=u.title),p.includes(c)&&(i+=2,a??=u.title)}}return i<=0?null:{entry:e,score:i,matchedSection:a}}function Bi(e,t=12){let n=e.trim();if(!n)return[];let o=$x(n),r=[];for(let s of vn.entries){let i=Mx(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 ji(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 Lr(e){return vn.entries.find(t=>t.id===e)}function Gi(e){let t=e.replace(/\\/g,"/").replace(/^\.\//,"");return vn.entries.find(n=>n.path===t||n.id===t)}function zi(e,t=1800){let n=[];e.summary&&n.push(e.summary);for(let r of e.sections){if(n.join(`
|
|
385
395
|
|
|
386
396
|
`).length>=t)break;n.push(`${r.title}
|
|
387
397
|
${r.body.slice(0,600)}`)}let o=n.join(`
|
|
388
398
|
|
|
389
|
-
`).trim();return o.length>t&&(o=`${o.slice(0,t-1)}\u2026`),o}function
|
|
390
|
-
`))}function
|
|
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>"),
|
|
392
|
-
`))}function
|
|
393
|
-
`);return
|
|
394
|
-
`))}async function
|
|
399
|
+
`).trim();return o.length>t&&(o=`${o.slice(0,t-1)}\u2026`),o}function Ji(e){return e.relatedCommands.find(n=>/\bhelp\b/i.test(n))??e.relatedCommands[0]}function Th(){return m(["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(`
|
|
400
|
+
`))}function yc(e,t){if(e.length===0)return m(`${t}
|
|
401
|
+
(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>"),m(n.join(`
|
|
402
|
+
`))}function wc(e,t){let n=Hi(vn.repoUrl,e.path),o=zi(e),r=e.relatedCommands.slice(0,8),s=Ji(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(`
|
|
403
|
+
`);return m(a)}var bc=new Map;function $h(e,t){bc.set(e,t)}function Mh(e){return bc.get(e)}function kc(e,t){let n=Number.parseInt(t,10);if(!Number.isFinite(n)||n<1)return null;let o=bc.get(e);return!o||n>o.length?null:o[n-1]}var qi;function Ph(e){qi=e}function Eh(e){let t=Number.parseInt(e,10);return!Number.isFinite(t)||t<1||!qi||t>qi.length?null:qi[t-1]}async function vc(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return{kind:"text",body:Th()};if(/^list\s*$/i.test(o)){let l=Mh(t);return l?.length?{kind:"text",body:yc(l,"Last search")}:{kind:"text",body:m("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:m("Usage: /docs search <topic>")};let u=Bi(l).map(ji);return $h(t,u),{kind:"text",body:yc(u,`Search: ${l}`)}}let s=/^follow\s+(\d+)\s*$/i.exec(o);if(s){let l=kc(t,s[1]);if(!l)return{kind:"text",body:m(`No result #${s[1]}. Run /docs search <topic> first.`)};let c=Lr(l.id);if(!c)return{kind:"text",body:m("Entry missing from index.")};let u=Ji(c);if(!u)return{kind:"text",body:m(`No related command for "${l.title}". Try /docs ${s[1]} for the doc excerpt.`)};let d=u.startsWith("/")?u:`/${u}`,p=await n(d);return p?.kind==="text",p}let i=/^(?:show\s+)?(\d+)\s*$/i.exec(o);if(i){let l=kc(t,i[1]);if(!l)return{kind:"text",body:m(`No result #${i[1]}. Run /docs search <topic> first.`)};let c=Lr(l.id);return c?{kind:"text",body:wc(c,Number.parseInt(i[1],10))}:{kind:"text",body:m("Entry missing from index.")}}let a=Gi(o);return a?{kind:"text",body:wc(a)}:{kind:"text",body:m("Unknown /docs command. /docs help")}}function Sc(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 Px(e,t){return`${e}:${t}`}function Ex(e,t){return`${e}:apps:${t}`}async function Ax(e,t){let n=e.trim();if(n===""||/^status$/i.test(n)||/^show$/i.test(n)||/^get$/i.test(n)){let a=S();return Xu({gatewayMode:a.gatewayMode,authPresent:pt(),tokenSet:!!Me(a),allowN:a.allowFrom.length,tgAllowN:a.telegramAllowFrom.length,updateBrief:$i(Cr())})}if(/^help$/i.test(n))return Q(Ma());let o=eo(n);if(!o)return fd();Qr(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 rd(r.gatewayMode,s,i)}function Ix(e){let t=e.trim();if(!xt(t))return od();let n=Xt(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.'),nd(o)}async function Ah(e,t,n){let o=se(t),r=Uu(n);if(r!==null){let a=Hu(o.cwd,r),l=Bu(a);return l.ok?(ss(t,a),m(`cwd: ${a}`)):m(`cd: ${l.error}`)}let s=await Wn(e.shell,n,{timeoutMs:e.syncTimeoutMs,maxBytes:e.syncMaxBytes,cwd:o.cwd}),i=[];return i.push(`$ ${n}`),s.stdout.trim()&&i.push(s.stdout.trimEnd()),s.stderr.trim()&&(i.push("\u2014 stderr \u2014"),i.push(s.stderr.trimEnd())),s.timedOut?i.push(`Timed out (${Math.round(e.syncTimeoutMs/1e3)}s limit).`):s.code!==0&&s.code!==null&&i.push(`Exit ${s.code}`),m(i.join(`
|
|
404
|
+
`))}async function Ih(e,t,n,o,r,s,i,a,l,c,u,d,p){if(u)return null;let f=Mf(s,p,e.recipesMaxTaskChars);return f?f.ok?await qn(e,t,n,o,r,{...i,text:f.text},a,l,c,!0,d):F(m(f.error)):null}async function qn(e,t,n,o,r,s,i,a,l=null,c=!1,u){let d=s.text.trim(),p=s.peerKey,f=d.match(/^!!\s*(start|stop)\s*$/i);if(f)return f[1].toLowerCase()==="start"?(r.set(p,!0),F(dd())):(r.set(p,!1),F(m("Free shell mode off.")));if(d.startsWith(e.commandPrefix)){let k=d.slice(e.commandPrefix.length).trim();if(!k)return F(m(`Send ${e.commandPrefix}<command> or /help`));let x=await Ih(e,t,n,o,r,p,s,i,a,l,c,u,k);return x!==null?x:F(await Ah(e,p,k))}if(/^\/help\s+files$/i.test(d.trim()))return F(Aa());let h=d.match(/^\/help\s+search\b(?:\s+([\s\S]*))?$/i);if(h){let k=(h[1]??"").trim();return await vc(k?`search ${k}`:"help",p,T=>qn(e,t,n,o,r,{...s,text:T},i,a,l,c,u))}let g=d.match(/^\/docs\b(?:\s+([\s\S]*))?$/i);if(g)return await vc(g[1]??"help",p,x=>qn(e,t,n,o,r,{...s,text:x},i,a,l,c,u));if(d==="/help"||d==="help")return F(Q(ao(e)));if(d.startsWith("/")){if(/^\/files(?:\s+help)?$/i.test(d.trim()))return F(Aa());let k=d.match(/^\/receive\b(?:\s+(\S+))?$/i);if(k){let $=(k[1]??"status").toLowerCase(),L=new Set(["here","cwd","session","dir"]),te=new Set(["default","global","reset"]);if(L.has($)){ka(p,"sessionCwd");let be=se(p).cwd;return F(m(`Inbound files will save under this chat\u2019s session folder:
|
|
395
405
|
${be}
|
|
396
406
|
(layout: \u2026/<peer>/<date>/<file> under that root).
|
|
397
407
|
|
|
398
|
-
Change folder with ${e.commandPrefix}cd \u2026 Send /receive default to use the server config again.`))}return
|
|
399
|
-
Notify on completion: on`:"";return F(
|
|
400
|
-
[cwd: ${
|
|
401
|
-
/log ${
|
|
402
|
-
/tail ${
|
|
403
|
-
${
|
|
404
|
-
|
|
405
|
-
`)))}let
|
|
406
|
-
(Gateway-shared recipe exists: /run show --global ${
|
|
407
|
-
(This chat stores an override: /run show --chat ${
|
|
408
|
-
/run remove --global ${
|
|
409
|
-
`))}if(/^queue$/i.test(i))return
|
|
410
|
-
`).trim(),$e=
|
|
411
|
-
(Shared shortcut exists: /shortcut show --global ${g})`:"",
|
|
412
|
-
(This chat overrides the name: /shortcut show --chat ${g})`:""
|
|
413
|
-
/shortcut remove --global ${
|
|
414
|
-
`,{mode:384})}catch(_){
|
|
415
|
-
${
|
|
408
|
+
Change folder with ${e.commandPrefix}cd \u2026 Send /receive default to use the server config again.`))}return te.has($)?(ka(p,"default"),F(m("Per-chat inbound folder cleared. Uploads now follow fileReceiveRootMode in config.json (/files)."))):F($==="help"?Ia():$==="status"?ld(e,p):Ia())}if(d==="/reload"||d==="/restart"){if(!a?.reload)return F(m("Reload is only available while the omnish gateway (omnish run) is running."));let $=await a.reload();return F(m($.ok?$.summary:`Reload failed: ${$.error}`))}if(d==="/updates"||d.startsWith("/updates ")){let $=d.slice(8).trim().toLowerCase();if($==="cached"||$==="last"){let te=Cr();return F(te?nr(te):m("No update snapshot yet. Send /updates (live check) once, or enable updateCheckEnabled and wait for the scheduled check."))}let L=await Rr(lt(),S());return F(nr(L))}let x=d.match(/^\/security(?:\s+(\S+))?\s*$/i);if(x){let $=(x[1]??"").toLowerCase(),L=S(),te=Ht(L);return F($==="help"||$==="?"?Q(Td()):$==="summary"||$==="brief"?m(Ku(te,"Send /security for the full report.")):$==="tips"?Q(Rd()):$===""||$==="full"||$==="report"?Cd(te):m("Unknown /security subcommand. Try /security, /security summary, /security tips, or /security help"))}let T=d.match(/^\/(gateway|gw|mode)\b(?:\s+(.*))?$/i);if(T){let $=(T[2]??"").trim();return F(await Ax($,a))}if(d==="/config"||d.startsWith("/config ")){let $=d.slice(7).trim();return F(await Ad($,a))}let O=Nf(d);if(O!==null)return F(await Ff(e,O));let C=Wf(d);if(C!==null)return await jf(e,C,p,t,u);let N=Df(d);if(N!==null)return await Gf(e,N,p,t,u);let I=_f(d);if(I!==null)return await Do(e,I,p,t,u);let K=Uf(d);if(K!==null)return await ic(e,K,p,t,u);let ee=Hf(d);if(ee!==null)return await zf(e,ee,p,t,u);let le=Bf(d);if(le!==null)return await Jf(e,le,p,t,u);let Se=Sc(d);if(Se!==null){let $=Jd(e,Se,l);return $===null?null:F($)}let _=d.match(/^\/(send|file)\b(?:\s+([\s\S]+))?$/i);if(_){let $=(_[2]??"").trim();if(!$)return F(Ea());let L=Pi($);if(!L)return F(Ea());let te=se(p).cwd,be=await Tr(te,L.selectorPart);if(be.length===0)return F(m(`No files matched: ${L.selectorPart}`));let Ke=await $r(be);if(!Ke.ok)return F(m(Ke.error));let Yn=Nn(e),Lt=[];for(let Eg of be){let Qn=ht(Eg,Yn);if("error"in Qn)return F(m(Qn.error));Lt.push({absPath:Qn.absPath,category:Qn.category,mimetype:Qn.mimetype,displayName:Qn.displayName,caption:L.caption})}return Lt.length===1?{kind:"file",spec:Lt[0]}:{kind:"files",specs:Lt}}if(d==="/allowlist"){let $=S();return F(td([{label:"allowFrom (WhatsApp)",items:$.allowFrom},{label:"telegramAllowFrom",items:$.telegramAllowFrom}]))}let ge=d.match(/^\/allow\b\s+(.+)$/i);if(ge)try{let $=Kr(ge[1].trim());return F(Pa($))}catch($){return F(m(String($)))}if(/^\/allow\b\s*$/i.test(d))return F(sd());let V=d.match(/^\/deny\b\s+(.+)$/i);if(V)try{let $=Yr(V[1].trim());return F(Pa($))}catch($){return F(m(String($)))}if(/^\/deny\b\s*$/i.test(d))return F(id());let P=d.match(/^\/(whatsapp|wa|telegram|tg)\b(?:\s+(.*))?$/i);if(P){let $=P[1].toLowerCase(),L=(P[2]??"").trim(),te=$==="whatsapp"||$==="wa";if($==="telegram"||$==="tg"){let Ke=L.match(/^token\s+(\S+)\s*$/i);return Ke?F(Ix(Ke[1]??"")):L===""||/^help$/i.test(L)?F(Q(ed(S()))):F(ud())}if(te)return L===""||/^help$/i.test(L)?F(Q(Zu(e))):F(cd())}let G=d.match(/^\/(cowork|cw)\b(?:\s+([\s\S]*))?$/i);if(G){let $=(G[2]??"").trim();return F(await gh($,p,e))}let Y=d.match(/^\/watch\b(?:\s+([\s\S]*))?$/i);if(Y){let $=(Y[1]??"").trim(),L=xh($,p);return L.replies.length===1?F(L.replies[0]):{kind:"texts",bodies:L.replies}}if(d==="/apps"||d.startsWith("/apps "))return F(await Wx(d,p,e,i,o));let z=Lx(d);if(z!==null)return F(await Fx(z,p,e,i,s.mediaSavedPath,u));if(d.startsWith("/bg")){let $=d.slice(3).trim();if(!$)return F(ad());let L=Rm($);if("error"in L)return F(m(L.error==="empty"?"Usage: /bg <command> or /bg -n <name> <command>.":L.error));let te=se(p).cwd,{id:be,meta:Ke}=t.spawnJob(e.shell,L.cmd,{cwd:te,name:L.name,notifyPeerKey:L.notify?p:null}),Yn=Ke.name?`${be} (${Ke.name})`:be,Lt=L.notify?`
|
|
409
|
+
Notify on completion: on`:"";return F(m(`Job ${Yn} started.
|
|
410
|
+
[cwd: ${te}]
|
|
411
|
+
/log ${Ke.name??be}
|
|
412
|
+
/tail ${Ke.name??be}${Lt}`))}if(d==="/tunnels")return F(await gc("list",e,Gn()));let ie=d.match(/^\/tunnel\b(?:\s+([\s\S]*))?$/i);if(ie){let $=(ie[1]??"").trim();return F(await gc($||"help",e,Gn()))}if(d==="/jobs"){let $=t.list().slice(0,20);return $.length===0?F(m("(no jobs yet)")):F(m($.map(L=>{let te=L.finishedAt&&L.startedAt?`${((Date.parse(L.finishedAt)-Date.parse(L.startedAt))/1e3).toFixed(1)}s`:"\u2026";return`${L.name?`${L.id} ${L.name}`:L.id} ${L.status} exit=${L.exitCode??"?"} ${te}
|
|
413
|
+
${L.cmd.slice(0,120)}${L.cmd.length>120?"\u2026":""}`}).join(`
|
|
414
|
+
|
|
415
|
+
`)))}let J=d.match(/^\/log\s+(\S+)(?:\s+(\d+))?\s*$/i);if(J){let $=J[1],L=t.resolveJobRef($);if(!L.ok)return F(m(L.error));let te=L.id,be=J[2]?Number.parseInt(J[2],10):e.jobLogTailLines,Ke=Number.isFinite(be)&&be>0?Math.min(be,500):e.jobLogTailLines;return F(m(t.tailLog(te,Ke)))}let Be=d.match(/^\/tail\s+(\S+)\s*$/i);if(Be){let $=Be[1],L=t.resolveJobRef($);if(!L.ok)return F(m(L.error));let te=L.id,be=Px(p,te),Ke=n.get(be)??0,{text:Yn,nextOffset:Lt}=t.readSince(te,Ke);return n.set(be,Lt),F(Yn?m(Yn.trimEnd()||"(no new output)"):m(`(no new output; offset ${Lt})`))}let $e=d.match(/^\/kill\s+(\S+)\s*$/i);if($e){let $=$e[1],L=t.resolveJobRef($);return L.ok?F(m(t.kill(L.id))):F(m(L.error))}let Ne=d.match(/^\/(shortcut|shortcuts|alias|aliases)\b(?:\s+([\s\S]*))?$/i);if(Ne){let $=Ne[1].toLowerCase(),L=(Ne[2]??"").trim();return $==="shortcuts"&&!L?L="list":(($==="alias"||$==="aliases")&&!L||$==="shortcut"&&!L)&&(L="help"),F(await _x(L,p,e))}if(!c){let $=d.trim().replace(/^\//,""),L=await Ih(e,t,n,o,r,p,s,i,a,l,c,u,$);if(L!==null)return L}return F(pd(e))}let y=d.match(/^>(\S+)\s*(.*)$/s);if(y){let k=y[1],x=y[2]??"",T=await i.writeNamedLine(p,k,x);return T?F(m(T)):null}if(await i.writeFocusedLine(p,d))return null;let b=await qf(e,d,p,t,u);return b!==null?b:r.get(p)&&d?F(await Ah(e,p,d)):e.chatLlmFallbackEnabled&&e.chatLlmShellCommand.trim().length>0&&u?.onPlainTextLlmFallback?(u.onPlainTextLlmFallback(p,d),null):F(md(e,d))}function Lx(e){return e==="/run"||e.startsWith("/run ")?e.slice(4).trim():e==="/r"||e.startsWith("/r ")?e.slice(2).trim():null}function Ox(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 Nx(e){let n=e.trim().match(/^(\S+)\s+([\s\S]+)$/);if(!n)return null;let o=Ox(n[2]);return o?{recipe:n[1],task:o.task,queued:o.queued,attach:o.attach}:null}async function Fx(e,t,n,o,r,s){let i=e.trim();if(!i||/^help$/i.test(i))return Q(bd());let a=/^online\b([\s\S]*)$/i.exec(i);if(a)return ch((a[1]??"").trim(),t,n);let l=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(i);if(l){let b=uh(t,n,l[1],l[2]??"");if(!b.ok)return m(b.error);let k=await jo(b.body);return m(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:x}=Xp(b);if(x)return m(`Unknown /run list suffix: "${x}". Use: list | list --chat | list -p | list --global | list -g`);if(k==="merged")return kd(nm(t,n));let T=em(t,n,k);return xd(T,k)}let u=/^show\b([\s\S]*)$/i.exec(i);if(u){let b=(u[1]??"").trim(),{mode:k,remainder:x}=Vp(b),T=/^(\S+)\s*$/i.exec(x);if(!T?.[1])return m("Usage: /run show <name> \u2014 or show --global|-g|--chat|-p <name> (-g shared, -p private)");let O=T[1];if(k==="resolved"){let le=Ze(t,n,O);return le?Na(le):Fa(O)}let C=k==="global"?"global":"chat",N=Jt(C,t,n,O);if(N)return Na(N,C==="global"?"From gateway-shared recipes (--global).":"From this chat only (--chat).");let I=C==="chat"&&Jt("global",t,n,O)?`
|
|
416
|
+
(Gateway-shared recipe exists: /run show --global ${O})`:"",K=C==="global"&&Jt("chat",t,n,O)?`
|
|
417
|
+
(This chat stores an override: /run show --chat ${O})`:"",ee=C==="global"?`Unknown recipe "${O}" in gateway-shared storage.`:`Unknown recipe "${O}" in this chat storage.`;return m(`${ee}${I}${K}`)}let d=/^add\b([\s\S]*)$/i.exec(i);if(d){let{scope:b,remainder:k}=vl((d[1]??"").trim()),x=k.match(/^(\S+)\s+([\s\S]+)$/);if(!x)return m(b==="global"?'Usage: /run add --global <name> <command\u2026> [--template "\u2026"] \u2014 cmd must reference "$OMNISH_TASK"':"Usage: /run add [--global|-g|--chat|-p] <name> <command\u2026> \u2014 see /run help");try{let T=Sl(x[2],n.recipesMacroDefaultCommand);xo(t,x[1],T,b);let O=Et(x[1]),C=O.ok?O.normalized:x[1].toLowerCase(),N=Jt(b,t,n,C)??Ze(t,n,C);return N?rr(C,N,b):m("Recipe save failed.")}catch(T){return m(String(T))}}let p=/^set\b([\s\S]*)$/i.exec(i);if(p){let{scope:b,remainder:k,explicit:x}=kl((p[1]??"").trim()),T=Zp(k);if(T){if(x)return m("Cannot combine a leading scope flag (-g, --global, --chat, -p) with a trailing scope flag on the same line.");let N=Et(T.name);if(!N.ok)return m(N.error);let I=xl(t,N.normalized,T.target,n);if(!I.ok)return m(I.error);if(I.kind==="noop")return m(I.message);let K=Jt(I.target,t,n,N.normalized)??Ze(t,n,N.normalized);return K?rr(N.normalized,K,I.target):m("Recipe scope update failed.")}let O=k.match(/^(\S+)\s*$/);if(O?.[1]&&x){let N=Et(O[1]);if(!N.ok)return m(N.error);let I=xl(t,N.normalized,b,n);if(!I.ok)return m(I.error);if(I.kind==="noop")return m(I.message);let K=Jt(I.target,t,n,N.normalized)??Ze(t,n,N.normalized);return K?rr(N.normalized,K,I.target):m("Recipe scope update failed.")}let C=k.match(/^(\S+)\s+([\s\S]+)$/);if(!C)return m(b==="global"?'Usage: /run set --global <name> <command\u2026> [--template "\u2026"] \u2014 or scope-only: /run set --global <name> | /run set <name> -g':"Usage: /run set [--global|-g|--chat|-p] <name> <command\u2026> \u2014 or move scope without changing the body: /run set -g <name> | /run set <name> -p \u2014 see /run help");try{let N=Sl(C[2],n.recipesMacroDefaultCommand);xo(t,C[1],N,b);let I=Et(C[1]),K=I.ok?I.normalized:C[1].toLowerCase(),ee=Jt(b,t,n,K)??Ze(t,n,K);return ee?rr(K,ee,b):m("Recipe save failed.")}catch(N){return m(String(N))}}let f=/^(?:remove|rm|del)\b([\s\S]*)$/i.exec(i);if(f){let{scope:b,remainder:k}=vl((f[1]??"").trim()),x=k.match(/^(\S+)\s*$/);if(!x?.[1])return m(b==="global"?"Usage: /run remove --global <name> (aliases: rm, del)":"Usage: /run remove [--global|-g|--chat|-p] <name>");let T=x[1];return tm(t,T,b)?vd(T,b):b==="chat"&&Jt("global",t,n,T)?m(`No recipe "${T}" in this chat. There is a gateway-shared recipe with that name \u2014 remove it with:
|
|
418
|
+
/run remove --global ${T}`):Sd(T,b)}let h=/^queue\s+load\b([\s\S]*)$/i.exec(i);if(h){let b=(h[1]??"").trim(),k=Af(n),x=null,T=/^json(?:\s+([\s\S]+))?$/i.exec(b);if(T){let I=(T[1]??"").trim();if(!I)return m('Usage: /run queue load json [{"recipe":"\u2026","task":"\u2026"}, \u2026] \u2014 or { "tasks": [ \u2026 ] }');x=I}else if(b.length>0){let I=b;(I.startsWith('"')&&I.endsWith('"')||I.startsWith("'")&&I.endsWith("'"))&&(I=I.slice(1,-1));let K=se(t).cwd,ee=await Tr(K,I);if(ee.length===0)return m(`No files matched: ${I}`);if(ee.length>1)return m("Queue load: specify a single JSON file.");let le=await $r(ee);if(!le.ok)return m(le.error);let Se=sc(ee[0],k);if(!Se.ok)return m(Se.error);x=Se.text}else if(r){let I=sc(r,k);if(!I.ok)return m(I.error);x=I.text}else return m("Usage: /run queue load <file.json> \u2014 or /run queue load json [\u2026] \u2014 or attach a file with caption /run queue load");let O=If(x);if(!O.ok)return m(O.error);let C=Lf(t,n,O.jobs);if(!C.ok)return m(C.error);let N=[];for(let I of C.items)N.push(o.enqueueQueuedRun(t,I,n));return m(N.join(`
|
|
419
|
+
`))}if(/^queue$/i.test(i))return m(o.runQueueStatus(t));if(/^queue\s+resume\s*$/i.test(i))return m(o.resumeRunQueue(t,n));let g=Nx(i);if(g){let{recipe:b,task:k,queued:x,attach:T}=g,O=Rl(T,n),C=Ze(t,n,b);if(!C)return Fa(b);if(C.steps&&C.steps.length>0){let Se=se(t).cwd,_=C.steps.length,ge=C.steps;if(s?.sendToPeer){let V=s.sendToPeer;(async()=>{let P=[],G=!1;for(let z=0;z<ge.length;z++){let ie=ge[z];if(G){P.push({index:z,label:ie.label??`step ${z+1}`,cmd:ie.cmd,exitCode:null,timedOut:!1,skipped:!0,output:""});continue}n.progressUpdates&&await V(t,`Step ${z+1}/${_}: ${ie.label??`step ${z+1}`}\u2026`).catch(()=>{});let J=await Wn(n.shell,ie.cmd,{timeoutMs:n.syncTimeoutMs,maxBytes:n.syncMaxBytes,cwd:Se}),Be=[J.stdout,J.stderr].filter(Boolean).join(`
|
|
420
|
+
`).trim(),$e=J.code===0&&!J.timedOut,Ne={index:z,label:ie.label??`step ${z+1}`,cmd:ie.cmd,exitCode:J.code,timedOut:J.timedOut,skipped:!1,output:Be};P.push(Ne),n.progressUpdates&&await V(t,om(b,Ne,z,_)).catch(()=>{}),!$e&&!ie.continueOnFail&&(G=!0)}let Y=rm(b,P);await V(t,Y)})().catch(()=>{})}return m(`Runbook "${b}" started (${_} steps). Results will be sent when complete.`)}let N=C.taskEnv??"OMNISH_TASK";if(!So(C.command,N))return m(`Recipe "${b}" command must reference "$${N}".`);let I=vo(k,n.recipesMaxTaskChars);if(!I.ok)return m(I.error);let K=C.promptTemplate?Us(C.promptTemplate,N,I.task):I.task,ee={[N]:K};if(x){let Se={command:C.command,extraEnv:ee,recipeLabel:b,startOptions:O};return m(o.enqueueQueuedRun(t,Se,n))}let le=Hs(b);return m(o.start(t,le,C.command,n,ee,O))}let y=i.match(/^(\S+)$/);if(y){let b=y[1],k=b.toLowerCase();return k==="add"||k==="set"?m('Usage: /run add <name> cmd [--template "\u2026"] \u2014 cmd must include "$OMNISH_TASK"'):k==="show"?m("Usage: /run show <name> \u2014 optional show --global|-g|--chat|-p <name>"):k==="remove"||k==="rm"||k==="del"?m("Usage: /run remove [--global|-g|--chat|-p] <name>"):Ze(t,n,k)?m(`Usage: /run ${b} <task text\u2026> \u2014 replaces <<<OMNISH_TASK>>> / $OMNISH_TASK in stored templates`):m(`Unknown recipe "${b}". /run list`)}return m("/run: could not parse. /run help")}async function _x(e,t,n){let o=e.trim();if(!o||/^help$/i.test(o))return Q(La());let r=/^online\b([\s\S]*)$/i.exec(o);if(r)return Bo((r[1]??"").trim(),t,n,lh);let s=/^(\S+)\s+publish\b([\s\S]*)$/i.exec(o);if(s){let d=dh(t,s[1],s[2]??"");if(!d.ok)return m(d.error);let p=await jo(d.body);return m(p.ok?p.message:p.error)}let i=/^list\b([\s\S]*)$/i.exec(o);if(i){let d=(i[1]??"").trim(),{filter:p,bad:f}=xf(d);return f?m(`Unknown /shortcut list suffix: "${f}". Use: list | list --chat | list -p | list --global | list -g`):hd(Rf(t,p))}let a=/^show\b([\s\S]*)$/i.exec(o);if(a){let d=(a[1]??"").trim(),{mode:p,remainder:f}=Sf(d),h=/^(\S+)\s*$/i.exec(f);if(!h?.[1])return m("Usage: /shortcut show <name> \u2014 or show --global|-g|--chat|-p <name> (-g shared, -p private)");let g=h[1];if(p==="resolved"){let O=Ii(t,g);if(!O)return yd(g);let C=O.scope==="global"?"Shared shortcut (all chats use it unless this chat overrides the name).":"This chat only.";return Oa(g,O.body,C)}let y=p==="global"?"global":"chat",b=Kt(y,t,g);if(b!==void 0)return Oa(g,b,y==="global"?"From the shared shortcut list (--global / -g).":"From this chat only (--chat / -p).");let k=y==="chat"&&Kt("global",t,g)!==void 0?`
|
|
421
|
+
(Shared shortcut exists: /shortcut show --global ${g})`:"",x=y==="global"&&Kt("chat",t,g)!==void 0?`
|
|
422
|
+
(This chat overrides the name: /shortcut show --chat ${g})`:"",T=y==="global"?`Unknown shortcut "${g}" in shared shortcuts.`:`Unknown shortcut "${g}" in this chat.`;return m(`${T}${k}${x}`)}let l=/^add\b([\s\S]*)$/i.exec(o);if(l){let{scope:d,remainder:p}=oc((l[1]??"").trim()),f=p.match(/^(\S+)\s+([\s\S]+)$/);if(!f)return m(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{Pr(t,f[1],f[2],d);let h=bt(f[1]),g=h.ok?h.normalized:f[1].trim().toLowerCase(),y=Kt(d,t,g)??"";return or(g,y,d)}catch(h){return m(String(h))}}let c=/^set\b([\s\S]*)$/i.exec(o);if(c){let{scope:d,remainder:p,explicit:f}=nc((c[1]??"").trim()),h=Cf(p);if(h){if(f)return m("Cannot combine a leading scope flag (-g, --global, --chat, -p) with a trailing flag on the same line.");let b=bt(h.name);if(!b.ok)return m(b.error);let k=rc(t,b.normalized,h.target);if(!k.ok)return m(k.error);if(k.kind==="noop")return m(k.message);let x=Kt(k.target,t,b.normalized)??"";return or(b.normalized,x,k.target)}let g=p.match(/^(\S+)\s*$/);if(g?.[1]&&f){let b=bt(g[1]);if(!b.ok)return m(b.error);let k=rc(t,b.normalized,d);if(!k.ok)return m(k.error);if(k.kind==="noop")return m(k.message);let x=Kt(k.target,t,b.normalized)??"";return or(b.normalized,x,k.target)}let y=p.match(/^(\S+)\s+([\s\S]+)$/);if(!y)return m(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{Pr(t,y[1],y[2],d);let b=bt(y[1]),k=b.ok?b.normalized:y[1].trim().toLowerCase(),x=Kt(d,t,k)??"";return or(k,x,d)}catch(b){return m(String(b))}}let u=/^(?:remove|rm|del)\b([\s\S]*)$/i.exec(o);if(u){let{scope:d,remainder:p}=oc((u[1]??"").trim()),f=p.match(/^(\S+)\s*$/);if(!f?.[1])return m(d==="global"?"Usage: /shortcut remove --global <name> (aliases: rm, del)":"Usage: /shortcut remove [--global|-g|--chat|-p] <name>");let h=f[1];return Tf(t,h,d)?gd(h,d):d==="chat"&&Kt("global",t,h)!==void 0?m(`No shortcut "${h}" in this chat. There is a shared shortcut with that name \u2014 remove it with:
|
|
423
|
+
/shortcut remove --global ${h}`):wd(h,d)}return Q(La())}async function Wx(e,t,n,o,r){let s=e.slice(5).trim(),i=s.toLowerCase();if(!i||i==="help")return Q(_a());let a=s.match(/^(\S+)\s*(.*)$/s);if(!a)return Q(_a());let l=a[1].toLowerCase(),c=(a[2]??"").trim();if(l==="online")return Bo(c,t,n,ih);let u=c.match(/^publish\b([\s\S]*)$/i);if(u){let d=l,p=o.getSessionCommand(t,d);if(!p)return m(`No running session "${d}" with a command. /apps list`);let f=mh(d,p,u[1]??"");if(!f.ok)return m(f.error);let h=await jo(f.body);return m(h.ok?h.message:h.error)}switch(l){case"start":{let d=c.match(/^(\S+)\s+([\s\S]+)$/);return d?m(o.start(t,d[1],d[2],n)):m("Usage: /apps start <name> <command\u2026>")}case"attach":{let d=c.split(/\s+/)[0];return d?m(o.attach(t,d)):m("Usage: /apps attach <name>")}case"detach":return m(o.detach(t));case"list":return m(o.list(t));case"info":case"get":{let d=c.split(/\s+/)[0];return m(o.info(t,d||void 0))}case"send":{let d=c.match(/^(\S+)\s+([\s\S]+)$/);return d?m(await o.sendText(t,d[1],d[2])):m("Usage: /apps send <name> <text\u2026>")}case"key":{let d=c.match(/^(\S+)\s+([\s\S]+)$/);return d?m(o.sendKey(t,d[1],d[2].trim())):m("Usage: /apps key <name> <KEY[,KEY\u2026]>")}case"tail":{let d=c.match(/^(\S+)(?:\s+(\d+))?\s*$/);if(!d)return m("Usage: /apps tail <name> [lines]");let p=d[2]?Number.parseInt(d[2],10):n.appsLogTailLines;return m(o.tail(t,d[1],p))}case"since":{let d=c.split(/\s+/)[0];if(!d)return m("Usage: /apps since <name>");let p=Ex(t,d),f=r.get(p)??0,{text:h,nextOffset:g}=o.readSince(t,d,f);return r.set(p,g),m(h.trimEnd()||"(no new log bytes)")}case"mute":{let d=c.split(/\s+/)[0];return d?m(o.mute(t,d)):m("Usage: /apps mute <name>")}case"unmute":{let d=c.split(/\s+/)[0];return d?m(o.unmute(t,d)):m("Usage: /apps unmute <name>")}case"raw":{let d=c.match(/^(\S+)\s+(on|off)\s*$/i);return d?m(o.setRaw(t,d[1],d[2].toLowerCase()==="on")):m("Usage: /apps raw <name> on|off")}case"resize":{let d=c.trim().split(/\s+/).filter(Boolean);if(d.length<3)return m("Usage: /apps resize <name> <cols> <rows>");let p=d[0],f=Number(d[1]),h=Number(d[2]);return!Number.isFinite(f)||!Number.isFinite(h)?m("cols and rows must be numbers."):m(o.resize(t,p,f,h))}case"stop":{let d=c.split(/\s+/)[0];return d?m(o.stop(t,d)):m("Usage: /apps stop <name>")}case"kill":{let d=c.split(/\s+/)[0];return d?m(o.kill(t,d)):m("Usage: /apps kill <name>")}case"rm":{let d=c.split(/\s+/)[0];return d?m(o.rm(t,d)):m("Usage: /apps rm <name>")}default:return m(`Unknown /apps subcommand "${l}". /apps help`)}}function Lh(e,t,n){return!e.clusterEnabled||Sc(t.trim())!==null?!0:n?zd(n,e):!1}async function Oh(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 Nh(){return m("Not allowlisted on this omnish device.")}async function Or(e,t,n,o,r,s,i,a,l,c,u,d){if((d?.surface??(s.peerKey.startsWith("tg:")?"telegram":"whatsapp"))==="telegram"){let f=Jr(e.telegramAllowFrom),h=s.peerKey.startsWith("tg:")?s.peerKey.slice(3):"";if(!h||!f.has(h)){M.warn({denied:s.peerKey,uid:h},"telegram denied"),await u({kind:"text",body:Nh()});return}}else{let f=zr(e.allowFrom),h=l.startsWith("wa:")&&/^wa:\+\d+$/.test(l)?l.slice(3):s.peerKey.replace(/^wa:/,""),g=oe(h)||"";if(!g||!f.has(g)){M.warn({denied:s.peerKey,phone:g,senderKey:l},"denied"),await u({kind:"text",body:Nh()});return}}try{if(!!!(s.mediaSavedPath||s.mediaError)&&!Lh(e,s.text,l))return;if(s.mediaError&&await u({kind:"text",body:m(s.mediaError)}),s.mediaSavedPath&&await u({kind:"text",body:m(`Saved: ${s.mediaSavedPath}`)}),s.text.trim()){let h=await qn(e,t,n,o,r,s,i,a,l,!1,c);h!==null&&await Oh(u,h)}}catch(f){M.error({err:String(f)},"inbound handler error"),await u({kind:"text",body:m(`Error: ${String(f)}`)}).catch(()=>{})}}function Ux(){if(process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{xc.readFileSync(me,"utf8").trim()===String(process.pid)&&xc.unlinkSync(me)}catch{}}async function Fh(e){process.env[ol]="1";let t=new Map,n=new Map,o=new Map,r=null,s=new Map,i=async(_,ge)=>{let V=s.get(_);r?.sendReply(_,ge,V)},a=new qt({onJobExit(_){_.notifyPeerKey&&i(_.notifyPeerKey,ri(_))}}),l=async(_,ge)=>{let V=_.startsWith("tg:")?"telegram":"whatsapp",P=s.get(_),G=$s(_,{kind:"file",spec:ge},P,V);r?.sendRoutedReply(_,G)},c=async(_,ge)=>{let V=s.get(_),P=_.startsWith("tg:")?"telegram":"whatsapp";r?.sendReply(_,ue(m(ge),P).text,V)},u={onPlainTextLlmFallback(_,ge){Ro(S(),_,ge,V=>i(_,V))},sendToPeer:i},d=new gn(()=>S(),i),p={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 bi({env:e,onReplyError:async(_,ge,V)=>{let P=_.startsWith("tg:")?"telegram":"whatsapp";r?.sendReply(_,ue(m(`Error sending: ${ge}`),P).text,V)},onMessage:async _=>{let ge=zl(),V=_.peerKey;s.set(V,_.messageId);let P=_.surface==="telegram"?"telegram":"whatsapp",G=_.senderE164&&P==="whatsapp"?`wa:${_.senderE164}`:V;to()&&M.info({peerKey:V,senderKey:G,surface:P},"platform inbound message");let Y=_.mediaSavedPath,z=_.mediaError;if(_.inboundMedia){let J=Vm(ge,V,_.inboundMedia);Y=J.mediaSavedPath??Y,z=J.mediaError??z}let ie={peerKey:V,text:_.text,...Y?{mediaSavedPath:Y}:{},...z?{mediaError:z}:{}};await Or(ge,a,t,n,o,ie,d,p,G,u,async J=>{try{if(J.kind==="bundle"){for(let $e of J.texts??[]){let Ne=ue($e,P);r?.sendReply(V,Ne.text,_.messageId)}for(let $e of J.files??[]){let Ne=$s(V,{kind:"file",spec:$e},_.messageId,P);r?.sendRoutedReply(V,Ne)}return}if(J.kind==="texts"){for(let $e of J.bodies){let Ne=ue($e,P);r?.sendReply(V,Ne.text,_.messageId)}return}if(J.kind==="text"){let $e=ue(J.body,P);r?.sendReply(V,$e.text,_.messageId);return}let Be=$s(V,J,_.messageId,P);r?.sendRoutedReply(V,Be)}catch(Be){r?.sendReply(V,`Error sending file: ${String(Be)}`,_.messageId)}},{surface:P})}}),process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{xc.writeFileSync(me,`${process.pid}
|
|
424
|
+
`,{mode:384})}catch(_){M.warn({err:String(_)},"could not write gateway pidfile")}mi({getCfg:()=>S(),getWaOutbound:()=>null,getTgSendMedia:()=>null,getTgSendText:()=>null,sendPlatformMedia:l,sendPlatformText:c});let f=null,h=S();if(h.webhookEnabled){let _=h.webhookToken||Dx.randomBytes(32).toString("hex");h.webhookToken||A({webhookToken:_}),f=fi({port:h.webhookPort,host:h.webhookHost,token:_},{sendToPeer:i,getDefaultPeerKey:()=>null}).stop}let g=Mi({getRunningVersion:lt,getConfig:S,log:M}),y=ti({getConfig:S,sendToPeer:i,sendMediaToPeer:l}),b=rs({getConfig:S,sendToPeer:i}),{deviceId:k,account:x}=await r.connect(),T=await yi(e,x??null),O=5*60*1e3,C=setInterval(()=>{yi(e,null).catch(()=>{})},O);C.unref?.();let N=e.deviceId?.trim();if(N){N!==k&&M.warn({configuredDeviceId:N,registeredDeviceId:k},"platform_device_id does not match registered device id");try{await ql(e,N)}catch(_){M.warn({err:String(_)},"could not set platform default device")}}let I=T?.gatewayMode??x?.gatewayMode,K=T?.connectors.whatsapp??x?.connectors?.whatsapp,ee=T?.connectors.telegram??x?.connectors?.telegram,le=[K?`whatsapp:${K.linked?"linked":K.status}`:null,ee?`telegram:${ee.linked?"linked":ee.status}`:null].filter(Boolean).join(", ");console.error(`omnish attached to platform (device ${k})`+(I?` \u2014 gatewayMode=${I}`:"")+(le?` \u2014 ${le}`:""));let Se=()=>{clearInterval(C),y(),b(),g?.(),f?.(),Ux(),Io(),r?.stop(),d.dispose(),a.killAllRunning(),Gn().stopAll().catch(()=>{}),console.error(`
|
|
425
|
+
${R(process.stderr,"shutting down\u2026")}`),process.exit(0)};process.on("SIGINT",Se),process.on("SIGTERM",Se),await new Promise(()=>{})}function Cc(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,ue(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,ue(o,"whatsapp").text);else n.kind==="text"&&await e.sendWaText?.(t.waJid,ue(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)}}ce();At();j();function Ki(){if(he()){let i=S(),a=Ht(i);return er(a)?{ok:!1,message:`Fix security errors before starting the gateway.
|
|
416
426
|
|
|
417
427
|
${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(`
|
|
418
428
|
`)}).join(`
|
|
419
429
|
|
|
420
|
-
`)}`}:{ok:!0}}let e=S(),t=e.gatewayMode,n=t==="whatsapp"||t==="both",o=t==="telegram"||t==="both",r=
|
|
421
|
-
config: ${W}`};if(n&&!pt())return{ok:!1,message:"WhatsApp enabled but no session. Run `omnish link` first."};let s=
|
|
430
|
+
`)}`}:{ok:!0}}let e=S(),t=e.gatewayMode,n=t==="whatsapp"||t==="both",o=t==="telegram"||t==="both",r=Me(e);if(o&&!r)return{ok:!1,message:`Telegram enabled (gatewayMode) but no bot token. Set telegramBotToken in config or TELEGRAM_BOT_TOKEN.
|
|
431
|
+
config: ${W}`};if(n&&!pt())return{ok:!1,message:"WhatsApp enabled but no session. Run `omnish link` first."};let s=Ht(e);return er(s)?{ok:!1,message:`Fix security errors before starting the gateway (or change gatewayMode / token).
|
|
422
432
|
|
|
423
433
|
${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(`
|
|
424
434
|
`)}).join(`
|
|
425
435
|
|
|
426
|
-
`)}`}:{ok:!0}}
|
|
427
|
-
`))}function
|
|
428
|
-
`));return}let i=
|
|
429
|
-
`));return}if(n==="list"){let o=await
|
|
430
|
-
`))}async function
|
|
431
|
-
`));return}let r=
|
|
432
|
-
`,{mode:384});try{
|
|
433
|
-
`,{mode:384});try{
|
|
434
|
-
`,{mode:384}),o}
|
|
435
|
-
`,{mode:384})}function
|
|
436
|
-
|
|
437
|
-
`);let
|
|
438
|
-
|
|
439
|
-
`)});r.on("close",()=>{
|
|
440
|
-
`)}
|
|
441
|
-
`))}function
|
|
442
|
-
`)}:{error:null,kind:"text",recipientsSent:d.size+
|
|
443
|
-
`)};let c=i.size+a.size,u=s.length;return{error:null,kind:"media",recipientsSent:c,filesSent:u,messagesSent:c*u}}async function
|
|
444
|
-
`+
|
|
445
|
-
`)));return}if(e.kind==="bundle"){for(let n of e.texts??[]){let o=
|
|
446
|
-
`)||
|
|
447
|
-
`)}),u=async
|
|
448
|
-
`)}),d.prompt()}function
|
|
436
|
+
`)}`}:{ok:!0}}At();j();At();import Xi from"node:fs";import Zi from"node:path";ce();import{spawn as Hx}from"node:child_process";import _h from"node:fs";import{stdout as Bx}from"node:process";At();j();Tn();var Rc=["tunnelRelayUrl","platformToken","platformDeviceId"],Wh=[{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 Qi(e){let t=[q(e,"omnish config"),w(e,"Manage ~/.omnish/config.json (env vars still override for platform credentials)."),"",q(e,"Usage:"),` ${v(e,"omnish config add <key> <value> [key value ...]")}`,` ${v(e,"omnish config show <key>[,<key>|*]")}`,` ${v(e,"omnish config edit")}`,` ${v(e,"omnish config edit <key> <value>")}`,` ${v(e,"omnish config delete <key>[,<key>]")}`,"",w(e,"Platform aliases: platform_url \u2192 tunnelRelayUrl, platform_token \u2192 platformToken,"),w(e," platform_device_id \u2192 platformDeviceId"),` ${v(e,"omnish config show platform")} ${w(e,"\u2014 attached-mode diagnostics (URL, token, devices)")}`,w(e," Full setup: omnish help platform"),w(e,"add sets/overwrites scalar keys (not array append). show * lists all keys."),""];console.log(t.join(`
|
|
437
|
+
`))}function Dh(e){return e.split(/[,\s]+/).map(t=>t.trim()).filter(Boolean)}function Tc(e){if(e.length===0)return[];let t=e.join(" ");if(t.includes(",")){let n=[];for(let o of t.split(",")){let r=o.trim();if(!r)continue;let s=r.split(/\s+/);if(s.length<2)throw new Error(`Invalid pair "${r}" \u2014 expected "key value"`);n.push({key:s[0],value:s.slice(1).join(" ")})}return n}if(e.length===2)return[{key:e[0],value:e[1]}];if(e.length%2===0){let n=[];for(let o=0;o<e.length;o+=2)n.push({key:e[o],value:e[o+1]});return n}return[{key:e[0],value:e.slice(1).join(" ")}]}function Yi(e,t){if(e==="clusterSenderBindings")return JSON.stringify(t.clusterSenderBindings);let n=String(t[e]??"");return e==="platformToken"||e==="telegramBotToken"?Mn(e,n):n}function Uh(e){return e==="tunnelRelayUrl"?{value:Sr(),source:Oo()}:e==="platformToken"?{value:Mn(e,Dl()),source:Lo()}:e==="platformDeviceId"?{value:(Ul()??"")||"(empty)",source:Hl()}:null}function jx(e){if(e!=="platformToken"&&e!=="tunnelRelayUrl")return;let n=S().platformToken.trim()||Dt()?.token?.trim()||"";n&&Tt({token:n,relayUrl:Sr()})}async function Gx(){let e=process.env.VISUAL?.trim()||process.env.EDITOR?.trim()||(process.platform==="win32"?"notepad":"nano"),t=_h.readFileSync(W,"utf8");await new Promise((n,o)=>{let r=Hx(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 _h.writeFileSync(W,t,{mode:384}),new Error(`Invalid config after edit: ${n instanceof Error?n.message:String(n)}`)}}async function Hh(e){let t=Bx,n=process.stderr,o=(e[0]??"").trim().toLowerCase(),r=e.slice(1);if(!o||o==="help"||o==="--help"||o==="-h"){Qi(t);return}if(o==="add"||o==="edit"){try{if(o==="edit"&&r.length===0){await Gx(),console.log(H(t,`Updated ${W}`));return}let s=Tc(r);if(s.length===0){console.error(R(n,"Expected at least one key/value pair.")),process.exitCode=1;return}for(let{key:i,value:a}of s){let l=hs(i);if(!l){console.error(R(n,`Unknown key "${i}". Try: ${Id().slice(0,8).join(", ")}\u2026 (omnish config show *)`)),process.exitCode=1;return}co(l,a),jx(l);let c=S(),u=l==="tunnelRelayUrl"?c.tunnelRelayUrl:Rc.includes(l)?Mn(l,String(c[l]??"")):Yi(l,c);console.log(H(t,`${i} \u2192 ${u}`))}console.log(H(t,W))}catch(s){console.error(R(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=[q(t,"Config"),w(t,W),""];for(let p of Wh){c.push(q(t,p.label));for(let f of p.keys){let h=Yi(f,l),g=Uh(f);g&&Rc.includes(f)?c.push(` ${v(t,f)}: ${h} ${w(t,`(effective: ${g.value}, source: ${g.source})`)}`):c.push(` ${v(t,f)}: ${h}`)}c.push("")}let u=new Set(Wh.flatMap(p=>p.keys)),d=Wa.filter(p=>!u.has(p));if(d.length>0){c.push(q(t,"Other"));for(let p of d)c.push(` ${v(t,p)}: ${Yi(p,l)}`)}console.log(c.join(`
|
|
438
|
+
`));return}let i=Dh(s),a=S();for(let l of i){let c=hs(l);if(!c){console.error(R(n,`Unknown key "${l}".`)),process.exitCode=1;return}let u=Yi(c,a),d=Uh(c);d&&Rc.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(R(n,"Expected at least one key to delete.")),process.exitCode=1;return}if(s==="*"){console.error(R(n,'Refusing "delete *". List keys explicitly.')),process.exitCode=1;return}try{for(let i of Dh(s)){let a=hs(i);if(!a){console.error(R(n,`Unknown key "${i}".`)),process.exitCode=1;return}Ld(a),console.log(H(t,`cleared ${i}`))}console.log(H(t,W))}catch(i){console.error(R(n,i instanceof Error?i.message:String(i))),process.exitCode=1}return}console.error(R(n,`Unknown subcommand "${o}".`)),Qi(t),process.exitCode=1}async function Bh(e){let t=he(),n=[];if(n.push(q(e,"platform")),!t){n.push(` ${w(e,"attached:")} ${we(e,"no")} \u2014 set platform_url + platform_token (omnish config add) or env`);let a=Oo(),l=Lo();return(a==="env"||l==="env")&&n.push(` ${w(e,"note:")} ${X(e,"partial env override (need URL + token)")}`),n}let o=Oo(),r=Lo(),s=Hl(),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,Mn("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(()=>(xr(),ef)),l=await a(t);n.push(` ${w(e,"gatewayMode:")} ${v(e,l.gatewayMode)} ${X(e,"(platform)")}`);let c=d=>{let p=l.connectors[d];return p?p.linked?v(e,"linked"):X(e,p.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}xr();At();function Gh(){let e=he();return e||(console.error(R(process.stderr,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1,null)}function zx(e){let t=e.trim();return t?t.startsWith("wa:")?{kind:"wa",value:t.slice(3).trim()}:t.startsWith("tg:")?{kind:"tg",value:t.slice(3).trim()}:/^\+?\d{8,}$/.test(t.replace(/\s/g,""))?{kind:"wa",value:t}:/^\d+$/.test(t)?{kind:"tg",value:t}:null:null}function jh(e){return e.replace(/\D/g,"")}async function zh(){let e=Gh();if(!e)return;let t=await jn(e),n=t.connectors.whatsapp,o=t.connectors.telegram;console.log(H(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 Jh(e){let t=Gh();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(`
|
|
439
|
+
`));return}if(n==="list"){let o=await jn(t);console.log(H(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 jn(t),r=[...o.allowFrom],s=[...o.telegramAllowFrom];for(let a of e.slice(1)){let l=zx(a);if(!l){console.error(R(process.stderr,`Unrecognized entry: ${a}`)),process.exitCode=1;return}if(l.kind==="wa"){let c=jh(l.value);c&&!r.includes(c)&&r.push(c)}else{let c=l.value.replace(/\D/g,"");c&&!s.includes(c)&&s.push(c)}}let i=await gi(t,{allowFrom:r,telegramAllowFrom:s});console.log(H(process.stdout,`Allowlists updated (${i.allowFrom.length} WhatsApp, ${i.telegramAllowFrom.length} Telegram).`));return}if(n==="set"){let o,r;for(let a=1;a<e.length;a++){let l=e[a];l==="--wa"&&e[a+1]?o=e[++a].split(",").map(c=>jh(c.trim())).filter(Boolean):l==="--tg"&&e[a+1]&&(r=e[++a].split(",").map(c=>c.trim().replace(/^tg:/i,"").replace(/\D/g,"")).filter(Boolean))}if(o===void 0&&r===void 0){console.error(R(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 gi(t,s);console.log(H(process.stdout,`Allowlists set (${i.allowFrom.length} WhatsApp, ${i.telegramAllowFrom.length} Telegram).`));return}console.error(R(process.stderr,`Unknown allow subcommand: ${e[0]}`)),process.exitCode=1}async function qh(e){let t="",n="",o="";for(let r=0;r<e.length;r++){let s=e[r];(s==="--url"||s==="-u")&&e[r+1]?t=e[++r].trim():(s==="--token"||s==="-t")&&e[r+1]?n=e[++r].trim():s==="--device-id"&&e[r+1]&&(o=e[++r].trim())}if(!t||!n){let r=Tc(e);for(let s of r)s.key==="platform_url"&&(t=s.value),s.key==="platform_token"&&(n=s.value),s.key==="platform_device_id"&&(o=s.value)}if(!t||!n){console.error(R(process.stderr,"Usage: omnish platform login --url <url> --token <token> [--device-id <id>]")),process.exitCode=1;return}co("tunnelRelayUrl",t.replace(/\/$/,"")),co("platformToken",n),o&&co("platformDeviceId",o),console.log(H(process.stdout,"Platform credentials saved to config.json.")),console.log(" Run: omnish platform status && omnish run")}import Jx from"qrcode-terminal";At();var qx=1e3,Kx=120;function Kh(){let e=he();return e||(console.error(R(process.stderr,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1,null)}function Mc(e){let t=e.replace(/\/$/,"");return/^https?:\/\//i.test(t)?t:`http://${t}`}async function Yh(e){let t=`${Mc(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 Yx(e){let t=await fetch(`${Mc(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 Qh(e){let t=process.stdout,n=w(t,"\xB7".repeat(42));console.log(X(t,"Scan with WhatsApp \u2192 Linked devices")),console.log(n),Jx.generate(e,{small:!0}),console.log(n)}var Vi="",$c=!1;function Qx(e){let t=e.status??"";return t==="qr"||t==="connecting"||t==="reconnecting"||t==="pairing_restart"}async function Vx(e){for(let t=0;t<Kx;t++){let n=await Yh(e);if(n.status==="linked"||n.linked)return n;if(n.status==="pairing_restart"&&!$c&&($c=!0,console.log(w(process.stdout,"Finishing WhatsApp link after scan (server restart) \u2014 wait a few seconds\u2026"))),n.qr&&n.qr!==Vi&&(Vi=n.qr,Qh(n.qr)),!Qx(n))throw new Error(n.error||n.statusMessage||`Unexpected status: ${n.status}`);await new Promise(o=>setTimeout(o,qx))}throw new Error("Timed out waiting for WhatsApp to link (scan the QR within ~2 minutes).")}async function Vh(){let e=Kh();if(!e)return;let t=await Yh(e);if(t.status==="linked"||t.linked){console.log(H(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(H(process.stdout,"Starting WhatsApp pairing on the platform\u2026")),$c=!1,Vi="",t=await Yx(e),t.qr&&(Qh(t.qr),Vi=t.qr),t=await Vx(e),console.log(H(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 Xh(){let e=Kh();if(!e)return;let t=await fetch(`${Mc(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(R(process.stderr,n.error||`HTTP ${t.status}`)),process.exitCode=1;return}console.log(H(process.stdout,`WhatsApp unlinked (status: ${n.status??"idle"}).`))}import Xx from"ws";function Zh(e,t,n){return new Promise(o=>{let r=new Xx(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 eg(e,t,n=12e3){let o=e.replace(/\/$/,""),r=No(o),s=await Zh(r,t,n),i,a,l="";for(let c of wi(o)){let u=await Zh(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 Zx=400,eC=8*1024*1024,tC=2*1024*1024;function nC(e){let t=ae,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=Zi.resolve(e[++r]))}return{authDir:t,force:n,help:o}}function oC(e){let t={},n=0,o=0;function r(s,i){let a;try{a=Xi.readdirSync(i,{withFileTypes:!0})}catch{return}for(let l of a){let c=l.name;if(c==="."||c==="..")continue;let u=Zi.join(i,c),p=(s?`${s}/${c}`:c).split(Zi.sep).join("/");if(!l.isSymbolicLink()){if(l.isDirectory())r(p,u);else if(l.isFile()){let f=Xi.readFileSync(u);if(f.length>tC)throw new Error(`file too large: ${p}`);if(n+=f.length,n>eC)throw new Error("total auth size exceeds limit (8 MiB)");if(o+=1,o>Zx)throw new Error("too many files (max 400)");t[p]=f.toString("base64")}}}}return r("",e),t}function ea(e){console.log([fe(e,"omnish platform"),w(e,"Attached mode: messengers on the hosted platform; shell on this machine (omnish run)."),"",q(e,"When to use"),w(e," Link WhatsApp/Telegram once on the platform dashboard. Run omnish run on laptops, servers, or containers without local Baileys."),"",q(e,"Prerequisites"),w(e," 1. Platform account token (signup/login on relay dashboard)."),w(e," 2. Messengers linked on the platform (dashboard QR or import-whatsapp)."),w(e," 3. allowFrom / telegramAllowFrom on dashboard Allowlists (Telegram: DM bot /id for your user id)."),"",q(e,"Configure"),` ${v(e,"omnish config add platform_url <url> platform_token <token>")}`,w(e," Optional: platform_device_id <id>"),w(e," Env overrides: OMNISH_PLATFORM_URL, OMNISH_TOKEN, OMNISH_DEVICE_ID"),w(e," (aliases: OMNISH_COMM_LAYER_URL, OMNISH_DEVICE_TOKEN, OMNISH_TUNNEL_*)"),"",q(e,"Workflow"),` ${v(e,"omnish platform login --url <url> --token <t>")} ${w(e,"\u2014 save credentials (or config add)")}`,` ${v(e,"omnish platform status")} ${w(e,"\u2014 account, connectors, allowlists")}`,` ${v(e,"omnish platform allow add tg:<id>")} ${w(e,"\u2014 or dashboard + bot /id")}`,` ${v(e,"omnish platform probe")} ${w(e,"\u2014 test WebSocket routing")}`,` ${v(e,"omnish run")} ${w(e,"\u2014 attach device; expect 'omnish attached to platform'")}`,"",q(e,"Subcommands"),v(e,"omnish platform login --url <url> --token <token> [--device-id <id>]"),w(e," Save platform URL and token to config.json."),"",v(e,"omnish platform status"),w(e," Show gatewayMode, connector status, allowlists, online devices."),"",v(e,"omnish platform allow list|add|set"),w(e," Manage platform allowlists (PUT /v1/me/allowlists)."),"",v(e,"omnish platform probe"),w(e," Test control-plane WebSocket routing (diagnose attached omnish run 400 errors)."),"",v(e,"omnish platform link-whatsapp"),w(e," Pair WhatsApp on the platform (terminal QR + poll until linked)."),"",v(e,"omnish platform unlink-whatsapp"),w(e," Remove platform WhatsApp session and auth files."),"",v(e,"omnish platform import-whatsapp [--auth-dir <path>] [--force]"),w(e," Upload local Baileys auth (after omnish link on this host). Use link-whatsapp or dashboard QR when possible."),w(e," Stop local omnish run before import to avoid WhatsApp session conflicts."),"",w(e,"Standalone omnish link on this host is not used for platform WhatsApp when attached."),w(e,"Docs: docs/guides/platform-reference.md (full API/CLI)"),w(e," docs/guides/platform-attached-mode.md (walkthrough)"),""].join(`
|
|
440
|
+
`))}async function rC(e){let{authDir:t,force:n,help:o}=nC(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(`
|
|
441
|
+
`));return}let r=he();if(!r){console.error(R(process.stderr,"Set OMNISH_PLATFORM_URL (control plane URL) and OMNISH_TOKEN (from dashboard signup/login).")),process.exitCode=1;return}if(!Xi.existsSync(t)){console.error(R(process.stderr,`Auth directory not found: ${t}`)),process.exitCode=1;return}re();let s=Zi.join(t,"creds.json");if(!Xi.existsSync(s)){console.error(R(process.stderr,`No creds.json in ${t} \u2014 link WhatsApp locally first (omnish link).`)),process.exitCode=1;return}let i;try{i=oC(t)}catch(f){console.error(R(process.stderr,String(f))),process.exitCode=1;return}if(Object.keys(i).length===0){console.error(R(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(R(process.stderr,d.error||`HTTP ${u.status}`)),process.exitCode=1;return}let p=d.qr?" QR emitted \u2014 open the platform dashboard if you need to scan.":"";console.log(H(process.stdout,`Uploaded Baileys auth. Platform connector status: ${d.status??"unknown"}.${p}`))}async function sC(){let e=process.stdout,t=process.stderr,n=he();if(!n){console.error(R(t,"Set platform_url + platform_token (omnish config add) or OMNISH_PLATFORM_URL + OMNISH_TOKEN.")),process.exitCode=1;return}console.log(`${fe(e,"Platform probe")} ${w(e,n.platformUrl)}`);let o=await eg(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(H(e,"Attached omnish run should be able to connect."));return}console.error(R(t,o.error??"Platform probe failed.")),o.controlWsOk&&!o.deviceWsOk&&console.error(R(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 tg(e){let t=(e[0]??"").toLowerCase();if(!t||t==="-h"||t==="--help"){ea(process.stdout);return}if(t==="login"){await qh(e.slice(1));return}if(t==="status"){await zh();return}if(t==="allow"){await Jh(e.slice(1));return}if(t==="probe"){await sC();return}if(t==="link-whatsapp"){await Vh();return}if(t==="unlink-whatsapp"){await Xh();return}if(t==="import-whatsapp"){await rC(e.slice(1));return}console.error(R(process.stderr,`Unknown platform subcommand: ${e[0]}`)),ea(process.stderr),process.exitCode=1}j();import rg from"node:crypto";import Kn from"node:fs";import sg from"node:path";function iC(){return rg.randomBytes(24).toString("hex")}function ng(){return rg.randomBytes(32).toString("hex")}function og(e){try{let t=Kn.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 aC(){let e=og(vt);if(e)return e;if(e=og(zo),e){U(sg.dirname(vt)),Kn.writeFileSync(vt,JSON.stringify(e,null,2)+`
|
|
442
|
+
`,{mode:384});try{Kn.unlinkSync(zo)}catch{}return e}return null}function ig(e){U(sg.dirname(vt));let t=aC(),n=(e??"").trim();if(n){let r={token:n,secret:t?.secret??ng()};Kn.writeFileSync(vt,JSON.stringify(r,null,2)+`
|
|
443
|
+
`,{mode:384});try{Kn.existsSync(zo)&&Kn.unlinkSync(zo)}catch{}return r}if(t)return t;let o={token:iC(),secret:ng()};return Kn.writeFileSync(vt,JSON.stringify(o,null,2)+`
|
|
444
|
+
`,{mode:384}),o}ce();import Sn from"node:fs";import mC from"node:http";import We from"node:path";import kt from"node:process";import{fileURLToPath as fC}from"node:url";import hC from"node:os";ot();j();import Pc from"node:crypto";var Ec="omnish_cfg_sess",ag=7*24*60*60*1e3;function Ac(e){let t=Date.now()+ag,n=Buffer.from(JSON.stringify({exp:t}),"utf8").toString("base64url"),o=Pc.createHmac("sha256",e).update(n).digest("hex");return`${n}.${o}`}function lg(e,t){let n=lC(t??"")[Ec];if(!n||!n.includes("."))return!1;let o=n.lastIndexOf("."),r=n.slice(0,o),s=n.slice(o+1),i=Pc.createHmac("sha256",e).update(r).digest("hex");try{let a=Buffer.from(s,"hex"),l=Buffer.from(i,"hex");if(a.length!==l.length||!Pc.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 Ic(e){let t=Math.floor(ag/1e3);return`${Ec}=${e}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${t}`}function cg(){return`${Ec}=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0`}function lC(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}j();import Lc from"node:fs";import ta from"node:process";function ug(e){try{return ta.kill(e,0),!0}catch{return!1}}function dg(e){let t=Date.now()+e;for(;Date.now()<t;);}function cC(){try{let e=Lc.readFileSync(Dr,"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 pg(e){Lc.writeFileSync(Dr,`${JSON.stringify(e)}
|
|
445
|
+
`,{mode:384})}function Nr(){try{Lc.unlinkSync(Dr)}catch{}}function mg(e){let t=cC();if(t&&t.port===e&&t.pid!==ta.pid){if(!ug(t.pid)){Nr();return}try{ta.kill(t.pid,"SIGTERM")}catch{}if(dg(350),ug(t.pid)){try{ta.kill(t.pid,"SIGKILL")}catch{}dg(100)}Nr()}}ce();j();import Oc from"node:fs";import uC from"node:process";function dC(e){try{return uC.kill(e,0),!0}catch{return!1}}function pC(){try{let e=Oc.readFileSync(me,"utf8").trim(),t=Number.parseInt(e,10);return Number.isFinite(t)&&t>0?t:null}catch{return null}}function na(){let e=pC();return e===null?!1:dC(e)}var Nc=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&&(Po(this.currentSock),this.currentSock=null)}beginPairing(t){if(this.busy)throw new Error("Pairing already in progress.");if(na())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.");re(),t.force&&(Oc.rmSync(ae,{recursive:!0,force:!0}),Oc.mkdirSync(ae,{recursive:!0,mode:448})),this.busy=!0,this.abort=new AbortController;let n=this.abort.signal;(async()=>{try{await Km({authDir:ae,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}})()}},Fr=new Nc;var pe="application/json; charset=utf-8",_r=null;function gC(){Nr();let e=_r;_r=null,e?e.close(()=>{kt.exit(0)}):kt.exit(0),setTimeout(()=>kt.exit(0),4e3).unref()}function yC(){let e=kt.env.OMNISH_CONFIG_UI_STATIC?.trim(),t=kt.env.OMNISH_UI_STATIC?.trim()||e;if(t&&Sn.existsSync(We.join(t,"index.html")))return t;let n=We.dirname(fC(import.meta.url)),o=We.join(n,"ui");if(Sn.existsSync(We.join(o,"index.html")))return o;let r=We.join(n,"..","..","dist","ui");if(Sn.existsSync(We.join(r,"index.html")))return r;let s=We.join(kt.cwd(),"dist","ui");if(Sn.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 wC(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 bC(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 Fc(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 kC(e){return ln.includes(e)}function _c(e){let t=(e.telegramBotToken??"").trim(),n=t.length===0?"":t.length<=8?"(set)":`${t.slice(0,4)}\u2026${t.slice(-4)}`;return{...e,telegramBotToken:n,telegramBotTokenConfigured:!!Me(e),telegramBotTokenEnvOverride:typeof kt.env.TELEGRAM_BOT_TOKEN=="string"&&kt.env.TELEGRAM_BOT_TOKEN.trim().length>0}}function vC(){let e=S();return{version:lt(),dataDir:D,configPath:W,waAuthDir:ae,whatsappLinked:pt(),gatewayPidHint:Sn.existsSync(We.join(D,"gateway.pid")),gatewayRunning:na(),gatewayLogFile:je,gatewayMode:e.gatewayMode,telegramBotTokenMasked:Me(e).length===0?"":_c(e).telegramBotToken,telegramBotTokenEnvOverride:typeof kt.env.TELEGRAM_BOT_TOKEN=="string"&&kt.env.TELEGRAM_BOT_TOKEN.trim().length>0}}function SC(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=Gr(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 xC(e){return typeof e=="string"?e:typeof e=="boolean"||typeof e=="number"?String(e):JSON.stringify(e)}async function fg(e){let t=yC(),{meta:n}=e;mg(e.port);let o=mC.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 p=i.searchParams.get("token")?.trim();if(p&&p===n.token){let f=Ac(n.secret);s.statusCode=302,s.setHeader("Location","/"),s.setHeader("Set-Cookie",Ic(f)),s.end();return}}if(a.startsWith("/api/")){let p=r.headers.cookie,f=lg(n.secret,p);if(r.method==="POST"&&a==="/api/session"){let h=await Fc(r),g=typeof h?.token=="string"?h.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=Ac(n.secret);s.statusCode=200,s.setHeader("Content-Type",pe),s.setHeader("Set-Cookie",Ic(y)),s.end(JSON.stringify({ok:!0}));return}if(r.method==="GET"&&a==="/api/me"){s.statusCode=f?200:401,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:f}));return}if(!f){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,...vC()}));return}if(r.method==="GET"&&a==="/api/config"){s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0,config:_c(S())}));return}if(r.method==="PUT"&&a==="/api/config"){let h=await Fc(r);if(!h||typeof h!="object"){s.statusCode=400,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!1,error:"Expected JSON object."}));return}let g=h;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(!xt(k))throw new Error("Invalid Telegram bot token format.");fs("telegramBotToken",k);continue}if(!kC(y))throw new Error(`Unknown or unsupported config key: ${y}`);fs(y,xC(b))}if("allowFrom"in g||"telegramAllowFrom"in g){let y=S();SC("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:_c(S())}));return}if(r.method==="POST"&&a==="/api/logout"){s.statusCode=200,s.setHeader("Content-Type",pe),s.setHeader("Set-Cookie",cg()),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(()=>{Fr.requestCancel(),gC()});return}if(r.method==="POST"&&a==="/api/gateway/start"){if(na()){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 h=Ki();if(!h.ok){s.statusCode=400,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!1,error:h.message}));return}let g=je,y=di(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 h=pi();switch(h.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:h.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:h.pid}));return;case"taskkill_ok":s.statusCode=200,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!0,pid:h.pid,taskkill:!0}));return;case"failed":s.statusCode=500,s.setHeader("Content-Type",pe),s.end(JSON.stringify({ok:!1,error:h.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
|
|
446
|
+
|
|
447
|
+
`);let h=Fr.subscribe(g=>{s.write(`data: ${JSON.stringify(g)}
|
|
448
|
+
|
|
449
|
+
`)});r.on("close",()=>{h()});return}if(r.method==="POST"&&a==="/api/wa/link/start"){let g=(await Fc(r))?.force===!0;try{Fr.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"){Fr.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=bC(t,l),u=We.join(t,"index.html");c&&Sn.existsSync(c)&&Sn.statSync(c).isFile()&&(u=c);let d=Sn.readFileSync(u);s.statusCode=200,s.setHeader("Content-Type",wC(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())}),_r=o,pg({pid:kt.pid,port:e.port,host:e.host,startedAt:new Date().toISOString()}),o.on("close",()=>{Nr(),_r===o&&(_r=null)})}function hg(e){let t=hC.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 TC from"node:readline";import yg from"node:path";import Oe from"node:process";ce();ot();ot();function Wc(e){return e.channel==="all"||e.channel==="whatsapp-all"||e.channel==="telegram-all"}function CC(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 RC(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=>oe(r.trim())).filter(r=>!!r);return o.length===0?null:{channel:"whatsapp",e164s:o}}if(e.includes(",")){let n=e.split(",").map(o=>oe(o.trim())).filter(o=>!!o);return n.length===0?null:{channel:"whatsapp",e164s:n}}if(e.startsWith("+")){let n=oe(e);return n?{channel:"whatsapp",e164s:[n]}:null}return null}function gg(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=RC(r);if(!i)return null;let a=CC(s);if(a===null)return null;if(a!==void 0)return{...i,mode:"text",body:a};let l=Pi(s);if(!l)return null;let{selectorPart:c,caption:u}=l;return{...i,mode:"media",selectorPart:c,caption:u}}function Dc(){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(`
|
|
450
|
+
`)}xr();At();j();var Go="wa:cli:interactive",$C={onPlainTextLlmFallback(e,t){Ro(S(),e,t,async n=>{n.trim()&&console.log(H(Oe.stdout,n))})}};function MC(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=oe(o);return r?`wa:${r}`:null}function PC(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=MC(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 Uc(e){let t=Oe.cwd(),n=[`${fe(e,"omnish i")} ${w(e,"[options]")}`,X(e,"Interactive shell \u2014 same commands as WhatsApp/Telegram chat."),"",q(e,"Usage:"),` ${v(e,"omnish i [options]")}`,` ${v(e,"omnish interactive [options]")}`,"",q(e,"Options:"),..._t(e," ",[{left:"--as <sender>",right:"Sender key for cluster commands (wa:+E164 or tg:id). Default: synthetic wa:cli:interactive."},{left:"-c, --command <line>",right:"Run one line and exit (non-interactive)."},{left:"-h, --help",right:"Show this help."}],o=>w(e,o)),"",`${w(e,"Trust:")} ${v(e,"Full local access like your shell; not gated by inbox allowlist.")}`,`${w(e,"Jobs:")} ${v(e,"/bg and /jobs apply only to this REPL session (not the gateway process).")}`,`${w(e,"Files:")} ${v(e,"Use /sendto for files or --text for plain messages through the gateway; plain /send needs a chat peer.")}`,`${w(e,"Gateway:")} ${v(e,"/reload and /updates require omnish run; /sendto requires omnish run for WA/TG delivery.")}`,"",Dc(),"",`${w(e,"cwd:")} ${v(e,`session starts at ${t} (change with !cd or ${S().commandPrefix}cd).`)}`];console.log(n.join(`
|
|
451
|
+
`))}function wg(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=oe(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 bg(e){let t="No recipients matched the requested /sendto destination.";return Wc(e)?he()?`${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 EC(e){let t=Wc(e)?await Jl():S();if(e.mode==="text"){let{waTargets:d,tgTargets:p}=wg(e,t);if(d.size===0&&p.size===0)return{error:bg(e)};let f=[];for(let h of d){let g=await Mt({op:"sendText",channel:"whatsapp",e164:h,text:e.body});g&&f.push(`[wa:${h}] ${g}`)}for(let h of p){let g=await Mt({op:"sendText",channel:"telegram",chatId:h,text:e.body});g&&f.push(`[tg:${h}] ${g}`)}return f.length>0?{error:f.join(`
|
|
452
|
+
`)}:{error:null,kind:"text",recipientsSent:d.size+p.size}}let n=se(Go).cwd,o=await Tr(n,e.selectorPart);if(o.length===0)return{error:`No files matched: ${e.selectorPart}`};let r=await $r(o);if(!r.ok)return{error:r.error};let s=o.map(d=>ht(d,t.fileSendMaxBytes));for(let d of s)if("error"in d)return{error:d.error};let{waTargets:i,tgTargets:a}=wg(e,t);if(i.size===0&&a.size===0)return{error:bg(e)};let l=[];for(let d of i)for(let p of s){if("error"in p)continue;let f=await Mt({op:"sendMedia",channel:"whatsapp",e164:d,absPath:p.absPath,caption:e.caption});f&&l.push(`[wa:${d}] ${p.displayName}: ${f}`)}for(let d of a)for(let p of s){if("error"in p)continue;let f=await Mt({op:"sendMedia",channel:"telegram",chatId:d,absPath:p.absPath,caption:e.caption});f&&l.push(`[tg:${d}] ${p.displayName}: ${f}`)}if(l.length>0)return{error:l.join(`
|
|
453
|
+
`)};let c=i.size+a.size,u=s.length;return{error:null,kind:"media",recipientsSent:c,filesSent:u,messagesSent:c*u}}async function AC(e,t,n,o,r,s,i){let a=e.trim();if(!a)return;let l=gg(a);if(l!==null||/^\/sendto(\s|$)/i.test(a)){if(l===null){console.log(R(Oe.stderr,`Invalid /sendto.
|
|
454
|
+
`+Dc()));return}let d=await EC(l);if(d.error!==null)console.log(R(Oe.stderr,d.error));else if(d.kind==="text")console.log(H(Oe.stdout,`Sent text to ${d.recipientsSent} recipient(s).`));else{let p=`Sent ${d.filesSent} file(s) to ${d.recipientsSent} recipient(s) (${d.messagesSent} message(s)).`;console.log(H(Oe.stdout,p))}return}let c={peerKey:Go,text:e},u=await qn(S(),t,n,o,r,c,s,void 0,i,!1,$C);u!==null&&await IC(u)}async function IC(e){if(e===null)return;if(e.kind==="file"||e.kind==="files"){console.log(H(Oe.stdout,["This CLI session has no chat peer to attach to.","Push through the gateway instead, for example:"," /sendto wa:+15551234567 ./my.pdf"," /sendto tg:123456789 ~/photo.jpg -- optional caption","(Requires `omnish run` on this machine.)"].join(`
|
|
455
|
+
`)));return}if(e.kind==="bundle"){for(let n of e.texts??[]){let o=ue(n,"whatsapp").text;o.trim()&&(console.log(o),console.log(""))}for(let n of e.files??[])console.log(H(Oe.stdout,`File: ${n.absPath}`));return}if(e.kind==="texts"){for(let n=0;n<e.bodies.length;n++){let o=ue(e.bodies[n],"whatsapp").text;o.trim()&&(n>0&&console.log(""),console.log(o))}return}if(e.kind!=="text")return;let t=ue(e.body,"whatsapp").text;t.trim()&&console.log(t)}async function kg(e){let t=PC(e);if(t.error==="help"){Uc(Oe.stdout);return}if(t.error&&t.error!==null){console.error(R(Oe.stderr,t.error)),console.error(w(Oe.stderr,"Try: omnish i --help")),Oe.exitCode=1;return}let{senderKey:n,oneShot:o}=t.opts,r=n??Go;re(),ss(Go,Oe.cwd());let s=new qt,i=new Map,a=new Map,l=new Map,c=new gn(()=>S(),async(f,h)=>{Oe.stdout.write(h),h.endsWith(`
|
|
456
|
+
`)||Oe.stdout.write(`
|
|
457
|
+
`)}),u=async f=>{try{await AC(f,s,i,a,l,c,r)}catch(h){console.error(R(Oe.stderr,String(h)))}};if(o!==null){await u(o),c.dispose(),s.killAllRunning();return}let d=TC.createInterface({input:Oe.stdin,output:Oe.stdout}),p=yg.basename(se(Go).cwd);d.setPrompt(`${p}> `),d.on("line",f=>{u(f).then(()=>{let h=yg.basename(se(Go).cwd);d.setPrompt(`${h}> `),d.prompt()})}),d.on("close",()=>{c.dispose(),s.killAllRunning(),Oe.stdout.write(`
|
|
458
|
+
`)}),d.prompt()}function Hc(){console.log(`omnish docs \u2014 search bundled documentation (offline)
|
|
449
459
|
|
|
450
460
|
omnish docs help
|
|
451
461
|
omnish docs search <topic>
|
|
@@ -453,22 +463,22 @@ ${s.filter(l=>l.severity==="error").map(l=>{let c=[l.message];return l.detail&&c
|
|
|
453
463
|
omnish docs show <path> e.g. docs/features/tunneling.md
|
|
454
464
|
|
|
455
465
|
Chat: /docs search <topic> \xB7 /docs <n> \xB7 /docs follow <n>
|
|
456
|
-
`)}function
|
|
466
|
+
`)}function Sg(e){let t=(e[0]??"help").toLowerCase(),n=e.slice(1).join(" ").trim();if(t==="help"||t==="-h"||t==="--help"){Hc();return}if(t==="search"){if(!n){console.error("[omnish] Usage: omnish docs search <topic>"),process.exitCode=1;return}let r=Bi(n).map(ji);if(Ph(r),r.length===0){console.log(`Search: ${n}
|
|
457
467
|
(no results)`);return}console.log(`Search: ${n}
|
|
458
468
|
`),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(`
|
|
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=
|
|
460
|
-
`))}function
|
|
461
|
-
`))}function
|
|
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
|
|
463
|
-
`))}function
|
|
464
|
-
`))}function
|
|
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
|
|
466
|
-
`),console.warn(
|
|
467
|
-
`,{mode:384})}catch(
|
|
468
|
-
`),
|
|
469
|
-
|
|
470
|
-
Updates (last check): ${
|
|
471
|
-
${
|
|
472
|
-
`))}async function
|
|
473
|
-
`));return}await
|
|
474
|
-
`)),r){let
|
|
469
|
+
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=Eh(n);if(!r){console.error("[omnish] No result #"+n+". Run omnish docs search first."),process.exitCode=1;return}let s=Lr(r.id);if(!s){console.error("[omnish] Entry missing from index."),process.exitCode=1;return}vg(s,Number.parseInt(n,10));return}let o=Gi(n);if(!o){console.error(`[omnish] No doc at path "${n}".`),process.exitCode=1;return}vg(o);return}console.error(`[omnish] Unknown docs subcommand "${t}". Try: omnish docs help`),process.exitCode=1}function vg(e,t){if(!e)return;let n=Hi(vn.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(zi(e)),console.log(""),e.relatedCommands.length){console.log("Try:");for(let r of e.relatedCommands.slice(0,8))console.log(` ${r}`)}}LC.setDefaultResultOrder("ipv4first");function Rg(){let e=process.stdout,t=[`${Ce(e,"omnish run")} ${w(e,"[options]")}`,X(e,"Listen for DMs and run shell commands from allowlisted chats."),"",q(e,"Usage:"),` ${v(e,"omnish run [options]")}`,"",q(e,"Options:"),..._t(e," ",[{left:"-d, --background",right:"Start the gateway detached; log to --log-file (default: <data>/logs/gateway.log)."},{left:"--log-file <path>",right:`Append stdout/stderr when background (default: ${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=he();n?t.push(q(e,"Attached mode (platform credentials detected):"),` ${w(e,"url:")} ${v(e,n.platformUrl)} ${X(e,`[${Oo()}]`)}`,` ${w(e,"token:")} ${v(e,Mn("platformToken",n.token))} ${X(e,`[${Lo()}]`)}`,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(`
|
|
470
|
+
`))}function Tg(){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(cs)),r=t.map((i,a)=>va(" ",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."),"",q(e,"Usage:"),` ${v(e,"omnish link [--force]")}`,` ${v(e,"omnish link --tg <bot_token>")}`,"",q(e,"Modes:"),...r,` ${X(e,"Do not combine --tg with --force.")}`,"",q(e,"Options:"),..._t(e," ",[{left:"-f, --force",right:"WhatsApp only: delete saved session before pairing."},{left:"-vb, --verbose",right:"Baileys debug logs on stderr during pairing (legacy: OMNISH_VERBOSE=1)."},{left:"-h, --help",right:"Show this help."}],i=>w(e,i)),"",`${v(e,"Next:")} ${fe(e,"omnish allow tg:<your_user_id>")} ${w(e,"then")} ${fe(e,"omnish run")}`,`${w(e,"Config:")} ${v(e,W)}`,""];he()&&s.push(q(e,"Platform attached mode:"),w(e," omnish link on this host is for standalone only. Link WhatsApp on the platform dashboard or:"),` ${v(e,"omnish platform import-whatsapp")} ${w(e,"(after a local omnish link, with omnish run stopped)")}`,w(e," Telegram: set bot token on the platform dashboard, not --tg here."),""),console.log(s.join(`
|
|
471
|
+
`))}function NC(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"){la(!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}
|
|
472
|
+
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 jc(){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."),"",q(e,"Usage:"),` ${v(e,"omnish [options] <command> [args...]")}`,"",q(e,"Options:"),..._t(e," ",o,s=>w(e,s)),"",q(e,"Commands:"),..._t(e," ",n,s=>w(e,s)),"",`${w(e,"Config:")} ${v(e,`${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(`
|
|
473
|
+
`))}function FC(e){let t=(e??"").trim().toLowerCase(),n=process.stdout;if(!t){jc();return}switch(t){case"link":Tg();return;case"run":Rg();return;case"service":Mg();return;case"pull":ml();return;case"i":case"interactive":Uc(n);return;case"ui":Pg();return;case"config":Qi(n);return;case"platform":ea(n);return;case"docs":Hc();return;default:console.error(R(process.stderr,`No detailed help for "${e}". Try: omnish help`)),process.exitCode=1}}function _C(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(R(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(R(l,`unknown run option: ${a}`)),console.error(R(l,"Try: omnish run --help")),process.exit(1)}}let s=n.trim()!==""?Bc.isAbsolute(n)?n:Bc.resolve(process.cwd(),n):je;return{background:t,logFile:s,help:o,verbose:r}}function WC(e,t){let n=di(e,{verbose:t});n.ok||(console.error(R(process.stderr,n.message)),process.exit(1));let o=process.stdout;console.log(`${H(o,`gateway started in background (pid ${n.pid}).`)} ${w(o,`Log: ${e}`)}`)}function DC(){let e=pi();switch(e.outcome){case"no_pidfile":console.error(R(process.stderr,`no pidfile at ${me} \u2014 is a background gateway running?`)),process.exitCode=1;return;case"invalid_pidfile":console.error(R(process.stderr,"invalid pidfile.")),process.exitCode=1;return;case"stale_cleaned":console.log(H(process.stdout,`process ${e.pid} is not running; removing stale pidfile.`));return;case"sent_signal":console.log(H(process.stdout,`sent SIGTERM to gateway (pid ${e.pid}).`));return;case"taskkill_ok":console.log(H(process.stdout,`stopped gateway (pid ${e.pid}) using taskkill.`));return;case"failed":console.error(R(process.stderr,e.message)),process.exitCode=1;return}}function xg(){if(process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{xn.readFileSync(me,"utf8").trim()===String(process.pid)&&xn.unlinkSync(me)}catch{}}function UC(e){return e.length<=8?"(set)":`${e.slice(0,4)}\u2026${e.slice(-4)}`}function HC(){if(!xn.existsSync(me))return"gateway process: not running (no pid file)";let e=xn.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 $g=120;function Mg(){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 ${$g}).`},{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)."),"",q(e,"Usage:"),` ${v(e,"omnish service <subcommand>")}`,"",q(e,"Subcommands:"),..._t(e," ",t,o=>w(e,o)),"",q(e,"Restart and reload:"),` ${v(e,"Process")} ${w(e,"\u2014 Linux user unit: Restart=on-failure, RestartSec=5. macOS: KeepAlive. Full restart loads config from disk.")}`,` ${v(e,"Config live")} ${w(e,"\u2014 allowlisted chat while gateway runs: /reload or /restart")}`,"",`${w(e,"Docs:")} ${v(e,"docs/guides/background-and-boot.md")} ${X(e,"\xB7")} https://omnish.dev`,""];console.log(n.join(`
|
|
474
|
+
`))}function BC(e){let t=process.stdout,n=process.stderr,o=(e[0]??"help").toLowerCase();if(o==="help"||o==="--help"||o==="-h"){Mg();return}if(o==="instructions"){let r=zt();if(r.error){console.error(R(n,r.error)),process.exitCode=1;return}console.log(ys(r));return}if(o==="status"){let r=zt(),s=(()=>{try{return xn.existsSync(me)?`gateway.pid: ${xn.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}
|
|
475
|
+
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,$g):80,i=Fs(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(R(n,"Install is disabled. Set serviceInstallFromChat to true in config (same trust as shell), then run again.")),process.exitCode=1;return}let s=Ls();s.ok?console.log(H(t,s.detail)):(console.error(R(n,s.detail)),process.exitCode=1);return}if(o==="uninstall"){if(!S().serviceInstallFromChat){console.error(R(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=Os();s.ok?console.log(H(t,s.detail)):(console.error(R(n,s.detail)),process.exitCode=1);return}console.error(R(n,`Unknown subcommand "${o}". Try: omnish service help`)),process.exitCode=1}function jC(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=oe(o);return r?`wa:${r}`:null}async function GC(){let e=null,t=Ki();t.ok||(console.error(R(process.stderr,t.message)),process.exit(1));let n=he();if(n){await Fh(n);return}let o=S(),r=o.gatewayMode,s=r==="whatsapp"||r==="both",i=r==="telegram"||r==="both",a=Me(o),l=Ht(o),c=process.stderr,u=Ju(l,"warn");if(u.length>0&&(console.warn(`${H(c,`Security (${u.length} finding(s)):`)}
|
|
476
|
+
`),console.warn(Ta(u,c))),process.env.OMNISH_BACKGROUND_GATEWAY==="1")try{xn.writeFileSync(me,`${process.pid}
|
|
477
|
+
`,{mode:384})}catch(P){M.warn({err:String(P)},"could not write gateway pidfile")}let d=(P,G)=>{let Y=S();if(!Y.clusterEnabled)return P;let z=at(),ie=(Y.clusterLabel??"").trim()||Cg.hostname(),J=null;if(G.startsWith("tg:"))J=G;else if(G){let $e=oe(G);$e&&(J=`wa:${$e}`)}let Be=J?Bt(Y,J):null;return Od(P,{nodeId:z,label:ie,role:Y.clusterRole,activeNodeId:Be?.nodeId??""})},p=new Map,f=new Map,h=new Map,g=null,y={stop:null,sendText:null,sendMedia:null},b=null,k=!1,x=async(P,G)=>{if(P.startsWith("wa:")){let Y=P.slice(3);g&&await g.sendText(Y,G)}else if(P.startsWith("tg:")){let Y=Number(P.slice(3));y.sendText&&Number.isFinite(Y)&&await y.sendText(Y,m(G))}},T=new qt({onJobExit(P){P.notifyPeerKey&&x(P.notifyPeerKey,ri(P))}}),O={onPlainTextLlmFallback(P,G){Ro(S(),P,G,Y=>x(P,Y))},sendToPeer:x},C=async(P,G)=>{if(P.startsWith("wa:")){let Y=P.slice(3);g&&await g.sendMedia(Y,G)}else if(P.startsWith("tg:")){let Y=Number(P.slice(3));y.sendMedia&&Number.isFinite(Y)&&await y.sendMedia(Y,G)}},N=()=>new gn(()=>S(),x),I=N();b=I;let K,ee={async reload(){try{M.info("gateway reload requested from chat"),await y.stop?.().catch(()=>{}),y.stop=null,y.sendText=null,y.sendMedia=null;let P=S(),G=P.gatewayMode==="telegram"||P.gatewayMode==="both",Y=Me(P);if(G&&Y){let J=await Nl(Y,()=>S(),K,{decorate:d});y.sendText=J.sendText,y.sendMedia=J.sendMedia,y.stop=J.stop}let z=["Reload complete.",`gatewayMode: ${P.gatewayMode}`,G&&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(`
|
|
478
|
+
`),ie=$i(Cr());return{ok:!0,summary:ie?`${z}
|
|
479
|
+
|
|
480
|
+
Updates (last check): ${ie}`:z}}catch(P){return{ok:!1,error:String(P)}}}};if(K=async(P,G)=>{let Y=S();await Or(Y,T,p,f,h,P,I,ee,P.peerKey,O,Cc({sendTg:G},{surface:"telegram"}),{surface:"telegram"})},i){let P=await Nl(a,()=>S(),K,{decorate:d});y.sendText=P.sendText,y.sendMedia=P.sendMedia,y.stop=P.stop}mi({getCfg:()=>S(),getWaOutbound:()=>g,getTgSendMedia:()=>y.sendMedia,getTgSendText:()=>y.sendText,sendPeerMedia:C});let le=null;{let P=S();if(P.webhookEnabled){let G=P.webhookToken||OC.randomBytes(32).toString("hex");P.webhookToken||A({webhookToken:G}),le=fi({port:P.webhookPort,host:P.webhookHost,token:G},{sendToPeer:x,getDefaultPeerKey:()=>{let z=S();return z.allowFrom.length>0?`wa:${Vt(z.allowFrom[0])}`:z.telegramAllowFrom.length>0?`tg:${z.telegramAllowFrom[0]}`:null}}).stop}}e=Mi({getRunningVersion:lt,getConfig:S,log:M});let Se=ti({getConfig:S,sendToPeer:x,sendMediaToPeer:C}),_=rs({getConfig:S,sendToPeer:x}),ge=!i,V=()=>{k=!0,Se(),_(),e?.(),e=null,le?.(),xg(),Io(),y.stop?.().catch(()=>{}),b?.dispose(),T.killAllRunning(),Gn().stopAll().catch(()=>{}),console.error(`
|
|
481
|
+
${R(process.stderr,"shutting down\u2026")}`),process.exit(0)};if(process.on("SIGINT",V),process.on("SIGTERM",V),s)for(;!k;){let P=!1,G;try{G=await ii({printQr:!1,verbose:to()}),await Bn(ai(G),3e5,"Gateway: timed out waiting for WhatsApp connection (5 min).")}catch(z){console.error(R(process.stderr,`connect failed: ${String(z)}`)),await new Promise(ie=>setTimeout(ie,5e3));continue}g=Bm(G,{decorate:d});let Y=Fm(G,async z=>{let ie=S(),J=z.fromE164||oe(z.fromJid)||"",Be=Kd(z),$e=`wa:${J}`;await Or(ie,T,p,f,h,Be,I,ee,$e,O,Cc({sendWaText:(Ne,$)=>g.sendText(Ne,$),sendWaMedia:(Ne,$)=>g.sendMedia(Ne,$)},{surface:"whatsapp",waJid:z.fromJid}),{surface:"whatsapp"})});if(await new Promise(z=>{let ie=J=>{J.connection==="close"&&(Il(J.lastDisconnect)===Hn.loggedOut&&(P=!0),G.ev.off("connection.update",ie),z())};G.ev.on("connection.update",ie)}),Y(),ge&&(I.dispose(),I=N(),b=I),Po(G),g=null,P&&(console.error(R(process.stderr,"session logged out. Run `omnish link` again.")),Se(),e?.(),e=null,xg(),Io(),y.stop?.().catch(()=>{}),process.exit(1)),k)break;await new Promise(z=>setTimeout(z,3e3))}else for(;!k;)await new Promise(P=>setTimeout(P,500));Se(),e?.(),Io(),y.stop?.().catch(()=>{}),I.dispose()}function zC(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(R(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(R(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(R(process.stderr,"--token requires a secret string.")),process.exit(1)),r=l;continue}let a=process.stderr;console.error(R(a,`unknown ui argument: ${i}`)),console.error(R(a,"Try: omnish ui --help")),process.exit(1)}return{help:t,host:n,port:o,token:r}}function Pg(){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."),"",q(e,"Usage:"),` ${v(e,"omnish ui [options]")}`,"",q(e,"Options:"),..._t(e," ",[{left:"--host <addr>",right:"Bind address (default 0.0.0.0 \u2014 reachable on LAN). Use 127.0.0.1 for loopback only."},{left:"--port <n>",right:"TCP port (default 3789)."},{left:"--token <secret>",right:`Set or rotate setup token (saved to ${vt}).`},{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(`
|
|
482
|
+
`))}async function JC(){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"){jc();return}if(e==="help"){FC(t[0]);return}switch(re(),e){case"link":{let n=NC(t);if(n.kind==="help"){Tg();return}if(n.kind==="error"){let o=process.stderr,r=n.message.replace(/^\[omnish\]\s*/,"");console.error(R(o,r)),process.exitCode=1;return}if(n.kind==="tg"){let o=n.token.trim(),r=process.stderr,s=process.stdout;if(!xt(o)){console.error(R(r,"That does not look like a Telegram bot token (expect digits:secret from @BotFather).")),process.exitCode=1;return}Xt(o);let i=pt()?"both":"telegram";Qr(i),console.log([`${fe(s,"Telegram")} ${v(s,"bot token saved to")} ${w(s,W)}`,`${w(s,"gatewayMode:")} ${fe(s,i)}`,"",v(s,"Next:"),` ${w(s,"1.")} ${v(s,"Find your numeric user id (e.g. t.me/userinfobot), then:")} ${fe(s,"omnish allow tg:<id>")}`,` ${w(s,"2.")} ${fe(s,"omnish run")}`,""].join(`
|
|
483
|
+
`));return}await Ym({verbose:to(),force:n.force});return}case"run":{let n=_C(t);if(n.help){Rg();return}if(n.verbose&&la(!0),n.background){WC(n.logFile,n.verbose);return}await GC();return}case"stop":DC();return;case"logout":{try{xn.rmSync(ae,{recursive:!0,force:!0}),console.log(H(process.stdout,"Session removed. Run `omnish link` to pair again."))}catch(n){console.error(R(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(R(r,"Usage: omnish allow +<E164> or omnish allow tg:<user_id>")),process.exitCode=1;return}try{let s=Kr(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(R(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(R(r,"Usage: omnish deny +<E164> or omnish deny tg:<user_id>")),process.exitCode=1;return}try{let s=Yr(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(R(r,String(s).replace(/^\[omnish\]\s*/,""))),process.exitCode=1}return}case"i":case"interactive":{await kg(t);return}case"status":{let n=process.stdout,o=S(),r=t.includes("--check-updates"),s=new qt().list(),i=s.filter(h=>h.status==="running").length,a=Me(o),l=Ht(o),c=o.gatewayMode==="whatsapp"||o.gatewayMode==="both",u=o.gatewayMode==="telegram"||o.gatewayMode==="both",d=pt(),p=d?Jm(ae):null,f=[];if(f.push(`${Ce(n,"omnish")} ${w(n,lt())}`,`${w(n,"gatewayMode:")} ${v(n,o.gatewayMode)}`,`${w(n,"data dir:")} ${v(n,Bc.dirname(ae))}`,"",`${w(n,"gateway process:")} ${v(n,HC().replace(/^gateway process: /,""))}`,"",sn(n),fe(n,"whatsapp"),` ${w(n,"in use:")} ${c?v(n,"yes"):we(n,"no (gatewayMode is telegram-only)")}`),c){let h=d?v(n,`linked (${ae})`):we(n,"missing \u2014 run omnish link");f.push(` ${w(n,"session:")} ${h}`),d&&p&&f.push(` ${w(n,"linked as:")} ${v(n,p)}`),d&&!p&&f.push(` ${w(n,"linked as:")} ${X(n,"(not in creds yet \u2014 try again after omnish link completes)")}`)}if(f.push(` ${q(n,"Allowed")}`),o.allowFrom.length===0)f.push(` ${X(n,"(none)")}`);else for(let h of o.allowFrom)f.push(` ${w(n,"whatsapp:")} ${v(n,h)}`);if(f.push("",sn(n),fe(n,"telegram")),f.push(` ${w(n,"in use:")} ${u?v(n,"yes"):we(n,"no (gatewayMode is whatsapp-only)")}`),u){let h=a?v(n,UC(a)):we(n,"(none) \u2014 omnish link --tg <token> or TELEGRAM_BOT_TOKEN");f.push(` ${w(n,"bot token:")} ${h}`)}if(f.push(` ${q(n,"Allowed")}`),o.telegramAllowFrom.length===0)f.push(` ${X(n,"(none)")}`);else for(let h of o.telegramAllowFrom)f.push(` ${w(n,"telegram:")} ${v(n,h)}`);if(f.push("",sn(n),...await Bh(n)),f.push("",sn(n),`${fe(n,"jobs")} ${w(n,`(recent): ${s.length} total, ${i} running`)}`,Yu(l,n)),console.log(f.join(`
|
|
484
|
+
`)),r){let h=await Rr(lt(),o),g=nr(h);console.log(""),console.log(sn(n)),console.log(ue(g,"whatsapp").text)}if(o.clusterEnabled){let h=at(),g=ke(),y=Object.keys(g.senderBindings).length,b=Object.keys(o.clusterSenderBindings??{}).length;console.log(""),console.log(sn(n)),console.log(fe(n,"cluster")),console.log(` ${w(n,"\xB7")} ${v(n,`enabled \xB7 label ${o.clusterLabel||Cg.hostname()} \xB7 bindings ${y} chat / ${b} default`)}`),console.log(` ${w(n,"node:")} ${v(n,`${h.slice(0,8)}\u2026`)}`)}return}case"commands":{let n=process.stdout,o=S();console.log(ju(ao(o),n)),console.log(""),console.log(sn(n)),console.log(v(n,"Keys editable from chat via /config set (same trust as shell):")),console.log(w(n,ln.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:")} ${fe(n,i)}`),s.clusterEnabled){let a=ke(),l=Ka(s,null);console.log(""),console.log(v(n,l.wa.replace(/\*([^*]+)\*/g,"$1").replace(/`([^`]+)`/g,"$1"))),console.log(""),console.log(qa(a,s,null));let c=Object.keys(a.senderBindings).length,u=Object.entries(s.clusterSenderBindings??{});if(c>0){console.log(""),console.log(fe(n,"Chat bindings (cluster-local.json)"));for(let[d,p]of Object.entries(a.senderBindings))console.log(` ${w(n,d)} ${X(n,"->")} ${v(n,`${p.nodeId} (${p.source}, since ${p.sinceIso})`)}`)}if(u.length>0){console.log(""),console.log(fe(n,"Config defaults (clusterSenderBindings)"));for(let[d,p]of u)console.log(` ${w(n,d)} ${X(n,"->")} ${v(n,p)}`)}}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(R(o,"Usage: omnish cluster use <senderE164|tg:id> <label-or-id>")),process.exitCode=1;return}let a=jC(s);if(!a){console.error(R(o,`Could not parse sender "${s}". Use +E164 (WhatsApp) or tg:<user_id> (Telegram).`)),process.exitCode=1;return}let{state:l,resolved:c}=qd(a,i);if(!c.ok){if(c.reason==="ambiguous-label"){let d=(c.matches??[]).map(p=>`${p.nodeId}(${p.label})`).join(", ");console.error(R(o,`Label "${i}" matches multiple machines: ${d}. Use the 8-character id.`))}else console.error(R(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(`${fe(n,"cluster:")} ${v(n,`${a} -> ${c.peer.nodeId} (${c.peer.label}).`)}`);let u=S();console.log(""),console.log(qa(l,u,a));return}if(r==="here"){console.error(R(o,"omnish cluster here is no longer available. Use: omnish cluster use <senderE164|tg:id> <label-or-id>")),console.error(R(o,"Or send /c here from the controller's chat on the machine you want to bind.")),process.exitCode=1;return}console.error(R(o,"Usage: omnish cluster [status | use <sender> <label-or-id>]")),process.exitCode=1;return}case"security":{let n=S(),o=Ht(n),r=t.includes("--json");console.log(r?qu(o):Ta(o,process.stdout)),er(o)&&(process.exitCode=1);return}case"service":{BC(t);return}case"pull":{await jp(t);return}case"media-exec":{await zp(t);return}case"pull-exec":{await Gp(t);return}case"config":{await Hh(t);return}case"tunnel":{await mf(t);return}case"platform":{await tg(t);return}case"docs":{Sg(t);return}case"ui":{let n=zC(t);if(n.help){Pg();return}if(!Number.isFinite(n.port)||n.port<1||n.port>65535){console.error(R(process.stderr,"port must be between 1 and 65535.")),process.exitCode=1;return}let o=ig(n.token);await fg({host:n.host,port:n.port,meta:o});let r=process.stdout,s=hg(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,vt)}`),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:jc(),e&&(process.exitCode=1)}}JC().catch(e=>{console.error(R(process.stderr,String(e))),process.exit(1)});
|