@shadowob/cloud 1.1.7 → 1.1.9

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.
Files changed (198) hide show
  1. package/README.md +159 -11
  2. package/dist/{agent-browser-CERTMCDL.js → agent-browser-EI7FIK3X.js} +3 -3
  3. package/dist/{agent-browser-CIRZRIY4.js → agent-browser-YXE4ES6Q.js} +3 -3
  4. package/dist/{agent-pack-LF3O5TR4.js → agent-pack-35TFCZKP.js} +1 -1
  5. package/dist/{agent-pack-RQT27V7R.js → agent-pack-UOG6ZAUL.js} +1 -1
  6. package/dist/agentmemory-KP5O7GHB.js +101 -0
  7. package/dist/agentmemory-UV74POU5.js +100 -0
  8. package/dist/{airtable-BG2Q75G2.js → airtable-CXXS3YUN.js} +3 -3
  9. package/dist/{airtable-JCQXFM5D.js → airtable-YZ5JR5JC.js} +3 -3
  10. package/dist/{alipay-TZQI34RB.js → alipay-WED5P3XC.js} +3 -3
  11. package/dist/{alipay-MZX2XCDB.js → alipay-WJTVREMG.js} +3 -3
  12. package/dist/{amap-KPCLZYYL.js → amap-AIN23TQ7.js} +3 -3
  13. package/dist/{amap-5RQB3VGC.js → amap-F6GF7QKB.js} +3 -3
  14. package/dist/{atlassian-LGOEWYC7.js → atlassian-G6PM6UVM.js} +3 -3
  15. package/dist/{atlassian-TVS2A4IU.js → atlassian-IDR2NPJC.js} +3 -3
  16. package/dist/{baidu-appbuilder-QRRL3ETM.js → baidu-appbuilder-JKQIA5TG.js} +3 -3
  17. package/dist/{baidu-appbuilder-6UMESXHW.js → baidu-appbuilder-KVMCIFYH.js} +3 -3
  18. package/dist/{baidu-maps-HEPMVP5D.js → baidu-maps-GWMXV6YT.js} +3 -3
  19. package/dist/{baidu-maps-HXC4FBVP.js → baidu-maps-ZXGT6QZM.js} +3 -3
  20. package/dist/{baidu-netdisk-G5Q6B5NH.js → baidu-netdisk-J5SLQVWW.js} +3 -3
  21. package/dist/{baidu-netdisk-RS2K5W2M.js → baidu-netdisk-PONM3DY6.js} +3 -3
  22. package/dist/{baidu-smartprogram-JHD3XWF6.js → baidu-smartprogram-WJ5JMO6V.js} +3 -3
  23. package/dist/{baidu-smartprogram-EWTK5WKK.js → baidu-smartprogram-Y5WBR7RX.js} +3 -3
  24. package/dist/{browserbase-IUIYVYI7.js → browserbase-CGVQHRC4.js} +3 -3
  25. package/dist/{browserbase-JFO2PCIA.js → browserbase-PVWYTEZC.js} +3 -3
  26. package/dist/{canva-3YOFL7JS.js → canva-COCBQAT2.js} +3 -3
  27. package/dist/{canva-FMYN65SM.js → canva-RSTJSEX5.js} +3 -3
  28. package/dist/{chunk-35LJYCQF.js → chunk-3CT6RQNM.js} +745 -482
  29. package/dist/{chunk-KODMGZUC.js → chunk-4YO3NA26.js} +1 -1
  30. package/dist/{chunk-RECNVWMT.js → chunk-6V7MW4HU.js} +17 -3
  31. package/dist/{chunk-C6OI4ZNO.js → chunk-EVV774KS.js} +1 -1
  32. package/dist/{chunk-SVMXSIMG.js → chunk-F6CQ6GAG.js} +2 -1
  33. package/dist/{chunk-JUPAE5IA.js → chunk-OL5VH6RN.js} +72 -69
  34. package/dist/{chunk-POSVEKIY.js → chunk-OYY64ZSX.js} +17 -3
  35. package/dist/{chunk-ZHVYNIHA.js → chunk-P5Y6F2NH.js} +745 -482
  36. package/dist/{chunk-EEFMJYKB.js → chunk-PSK2SYZ3.js} +2 -1
  37. package/dist/{chunk-6YAYCWGK.js → chunk-PYJRFKPN.js} +1 -1
  38. package/dist/{chunk-JY2HTT7Q.js → chunk-RMDY3W4V.js} +6 -0
  39. package/dist/{chunk-EWB7L7IW.js → chunk-X2SREECR.js} +6 -6
  40. package/dist/{chunk-LXJBQBGL.js → chunk-X5VOIA72.js} +6 -6
  41. package/dist/{chunk-CTNUKOQE.js → chunk-Y5BJ3EW2.js} +6 -0
  42. package/dist/{chunk-SAP2DBHO.js → chunk-Y6BKVDG7.js} +1 -1
  43. package/dist/{chunk-6P2K6QZR.js → chunk-ZGMWSSCC.js} +72 -69
  44. package/dist/{claude-plugin-577TAQVS.js → claude-plugin-FPN32WMT.js} +1 -1
  45. package/dist/{claude-plugin-L3MXJJ6J.js → claude-plugin-IYHOVTVL.js} +1 -1
  46. package/dist/cli.js +930 -149
  47. package/dist/{cloudflare-RDFPKMM5.js → cloudflare-U3RHJJKK.js} +3 -3
  48. package/dist/{cloudflare-HBBABPK6.js → cloudflare-ZHN7UGPX.js} +3 -3
  49. package/dist/{cnb-FLP3QX46.js → cnb-AMXC5I7D.js} +3 -3
  50. package/dist/{cnb-YAVVEYFB.js → cnb-P3IZ4JTD.js} +3 -3
  51. package/dist/console/index.html +1 -1
  52. package/dist/console/static/css/index.f4563d95.css +1 -0
  53. package/dist/console/static/js/index.020abc71.js +1 -0
  54. package/dist/{coze-E6VGRNLV.js → coze-66RYMKVB.js} +3 -3
  55. package/dist/{coze-C6PMDPBI.js → coze-YE3BINXP.js} +3 -3
  56. package/dist/{dashboard.command-ZMQFKLNQ.js → dashboard.command-BRPZCZER.js} +1 -1
  57. package/dist/{dashboard.command-2AM45SIT.js → dashboard.command-GUHSJ2CN.js} +1 -1
  58. package/dist/{dingtalk-JNRNRN7E.js → dingtalk-4RFQG7N2.js} +3 -3
  59. package/dist/{dingtalk-WZGGIAHJ.js → dingtalk-VNFKXD2P.js} +3 -3
  60. package/dist/{douyin-miniprogram-AIJPPIZH.js → douyin-miniprogram-UEALAGOS.js} +3 -3
  61. package/dist/{douyin-miniprogram-HCYZ5NBW.js → douyin-miniprogram-UNB6UO2I.js} +3 -3
  62. package/dist/{figma-2YYNSCDX.js → figma-A264OWU5.js} +3 -3
  63. package/dist/{figma-RYOBMENP.js → figma-Y4TGSDZP.js} +3 -3
  64. package/dist/{firebase-OYSY6HPT.js → firebase-AI3MAGYG.js} +3 -3
  65. package/dist/{firebase-2IJDDBXX.js → firebase-ZGQARUIH.js} +3 -3
  66. package/dist/{firecrawl-2T3SBUW7.js → firecrawl-2JW7DMTH.js} +3 -3
  67. package/dist/{firecrawl-IYYXLAZM.js → firecrawl-UURQ5P5N.js} +3 -3
  68. package/dist/{flyai-QS5Q6FJR.js → flyai-EJGDMYFA.js} +3 -3
  69. package/dist/{flyai-7FJ4TRAG.js → flyai-ZFMZBBHJ.js} +3 -3
  70. package/dist/{gitagent-MWI75OIX.js → gitagent-5SDBYFNA.js} +1 -1
  71. package/dist/{gitagent-YBMWY7NZ.js → gitagent-ODXPCR4X.js} +1 -1
  72. package/dist/{gitee-3N7OFOM7.js → gitee-5UMJ4BC7.js} +3 -3
  73. package/dist/{gitee-KVNK6KLZ.js → gitee-D6NAZTCO.js} +3 -3
  74. package/dist/{github-LUEC2LID.js → github-JBLDKIA3.js} +3 -3
  75. package/dist/{github-XRO5Z3GC.js → github-PZQAVEZP.js} +3 -3
  76. package/dist/{google-ads-VPKWTX67.js → google-ads-BIFQOJ5M.js} +3 -3
  77. package/dist/{google-ads-A3QAJI4D.js → google-ads-QU3LJE4O.js} +3 -3
  78. package/dist/{google-analytics-C4UR5ZR2.js → google-analytics-7VZ6YZVA.js} +3 -3
  79. package/dist/{google-analytics-XDYZA2B7.js → google-analytics-HXMPCL5V.js} +3 -3
  80. package/dist/{google-workspace-YX35SHHX.js → google-workspace-6SEBJ4VA.js} +2 -2
  81. package/dist/{google-workspace-LL3EWVHH.js → google-workspace-L3AMJLCF.js} +2 -2
  82. package/dist/{huawei-xiaoyi-KPWLTSHB.js → huawei-xiaoyi-C6QIJMPM.js} +3 -3
  83. package/dist/{huawei-xiaoyi-6BSMGJHR.js → huawei-xiaoyi-JGLXWU5P.js} +3 -3
  84. package/dist/{hubspot-FTIEMNZO.js → hubspot-LACJGE6D.js} +3 -3
  85. package/dist/{hubspot-DIUHGEDI.js → hubspot-XWPRO4KZ.js} +3 -3
  86. package/dist/{huggingface-UUXK2RHK.js → huggingface-26QQZK4C.js} +3 -3
  87. package/dist/{huggingface-MJCOXA7E.js → huggingface-CQICNA2R.js} +3 -3
  88. package/dist/index.d.ts +1338 -1
  89. package/dist/index.js +1364 -226
  90. package/dist/{inference-ai-image-generation-PXV6IG4U.js → inference-ai-image-generation-5KYIUWT6.js} +3 -3
  91. package/dist/{inference-ai-image-generation-CMI6R5T3.js → inference-ai-image-generation-J2NYDCLZ.js} +3 -3
  92. package/dist/{inference-sh-7AZOLEFI.js → inference-sh-5SWQTK73.js} +3 -3
  93. package/dist/{inference-sh-ABQOD3YF.js → inference-sh-PTQF6T3R.js} +3 -3
  94. package/dist/{init.command-YVG4X6II.js → init.command-C7UKPK2Y.js} +3 -3
  95. package/dist/{init.command-JKE3SXAS.js → init.command-UNL66BMR.js} +3 -3
  96. package/dist/{klaviyo-LDPBWBSS.js → klaviyo-4UNPMBFT.js} +3 -3
  97. package/dist/{klaviyo-6K5YEFNH.js → klaviyo-SLYNEULT.js} +3 -3
  98. package/dist/{kuaidi100-HGFM5VK2.js → kuaidi100-HZKV5AIS.js} +3 -3
  99. package/dist/{kuaidi100-UHPFCVXP.js → kuaidi100-ZQUW7GHH.js} +3 -3
  100. package/dist/lark-HQUZNHDI.js +382 -0
  101. package/dist/lark-PAV7XWJS.js +381 -0
  102. package/dist/{linear-T4ORUP7N.js → linear-PYGQ5SLK.js} +3 -3
  103. package/dist/{linear-7QFSFPOD.js → linear-VJLYNTUF.js} +3 -3
  104. package/dist/{lovart-PDUXRUHJ.js → lovart-KC6SVNAJ.js} +3 -3
  105. package/dist/{lovart-QO3SK55T.js → lovart-WVKY4RR4.js} +3 -3
  106. package/dist/{meta-ads-SCNFI45S.js → meta-ads-E6XT33GI.js} +3 -3
  107. package/dist/{meta-ads-V6XPZWX3.js → meta-ads-RJ6DWRYN.js} +3 -3
  108. package/dist/{miclaw-TPPPS2WN.js → miclaw-4BA3A2YN.js} +3 -3
  109. package/dist/{miclaw-5CNTW7VV.js → miclaw-LUV6DCHX.js} +3 -3
  110. package/dist/{model-provider-KFB76XV5.js → model-provider-SYXJZ3JD.js} +1 -1
  111. package/dist/{model-provider-AVSFJSZP.js → model-provider-U7NEYA3Y.js} +1 -1
  112. package/dist/nature-skills-G76ABIWZ.js +143 -0
  113. package/dist/nature-skills-SQHMFXKT.js +142 -0
  114. package/dist/{notion-WFA7KGZZ.js → notion-2JZAKOFP.js} +1 -1
  115. package/dist/{notion-FZK76MN2.js → notion-O3NO5TJH.js} +1 -1
  116. package/dist/{oceanengine-3JZUS3PP.js → oceanengine-D23UZGVB.js} +3 -3
  117. package/dist/{oceanengine-5BRIJVJE.js → oceanengine-WDK2OXX5.js} +3 -3
  118. package/dist/{opencli-PFXHGCS2.js → opencli-36P63YNU.js} +3 -3
  119. package/dist/{opencli-VIGRJTGH.js → opencli-SUHDFR33.js} +3 -3
  120. package/dist/{paypal-Z5JYHIWD.js → paypal-K27SUW3B.js} +3 -3
  121. package/dist/{paypal-33UADIPR.js → paypal-OPZ3KOV5.js} +3 -3
  122. package/dist/{playwright-SQAQ3DZG.js → playwright-2ULT3NIC.js} +3 -3
  123. package/dist/{playwright-MG5WHK47.js → playwright-3Q7LBILG.js} +3 -3
  124. package/dist/{plugins-HZBWK3WQ.js → plugins-2MITZ4ZD.js} +2 -2
  125. package/dist/{plugins-I4GD5SZX.js → plugins-UK2QWD6G.js} +2 -2
  126. package/dist/{posthog-MU5MAJOQ.js → posthog-E3EHXLAN.js} +3 -3
  127. package/dist/{posthog-RJRRKDWB.js → posthog-KPJVLGX6.js} +3 -3
  128. package/dist/{salesforce-34FVIJTG.js → salesforce-FGPNG7FB.js} +3 -3
  129. package/dist/{salesforce-3QZ6OFVO.js → salesforce-TVHISKBC.js} +3 -3
  130. package/dist/{sentry-PIWW46VA.js → sentry-BZ3J3MZM.js} +3 -3
  131. package/dist/{sentry-MCIRMACU.js → sentry-XC57YRAJ.js} +3 -3
  132. package/dist/{seo-suite-WJXMA3S4.js → seo-suite-2MDEDLAB.js} +3 -3
  133. package/dist/{seo-suite-4SQ3I67Q.js → seo-suite-U75O3QP6.js} +3 -3
  134. package/dist/{serve.command-XLBJUOV6.js → serve.command-G5RVQFUD.js} +3 -3
  135. package/dist/{serve.command-RD6I6MFD.js → serve.command-PYGDG7K3.js} +3 -3
  136. package/dist/{shadowob-PRSMI5MW.js → shadowob-3QZ7DLDW.js} +158 -31
  137. package/dist/{shadowob-JELOWHWX.js → shadowob-CJLOEKFP.js} +158 -31
  138. package/dist/{sherlock-2PKY2E2Y.js → sherlock-CQFUHKDH.js} +3 -3
  139. package/dist/{sherlock-C5ZWPPVT.js → sherlock-DONK2I6E.js} +3 -3
  140. package/dist/{shopify-GL3NFVGE.js → shopify-NO5GI3WD.js} +3 -3
  141. package/dist/{shopify-R4G3UXM6.js → shopify-VW2KLKH5.js} +3 -3
  142. package/dist/{skill-discovery-YPXXV622.js → skill-discovery-6JEPPKKM.js} +3 -3
  143. package/dist/{skill-discovery-7INAUP4D.js → skill-discovery-PWRAVGIS.js} +3 -3
  144. package/dist/skills/shadowob-cli/SKILL.md +7 -0
  145. package/dist/{stripe-LJNPQ3CQ.js → stripe-HCNCKG4C.js} +1 -1
  146. package/dist/{stripe-C22RR4ZS.js → stripe-IU3KTJ4H.js} +1 -1
  147. package/dist/{supabase-IRNQ54FJ.js → supabase-KRL7JW2D.js} +3 -3
  148. package/dist/{supabase-N4ONFJNQ.js → supabase-TYEBTZNO.js} +3 -3
  149. package/dist/{taobao-aipaas-LRR4GMO3.js → taobao-aipaas-PEUIDOYP.js} +3 -3
  150. package/dist/{taobao-aipaas-RVKORSF4.js → taobao-aipaas-SA5E4MZA.js} +3 -3
  151. package/dist/{tapd-TMQRSMFG.js → tapd-6DDIUPVQ.js} +3 -3
  152. package/dist/{tapd-3JPVJ7XH.js → tapd-OTYLSZGY.js} +3 -3
  153. package/dist/{tencent-ads-UHC6OPBV.js → tencent-ads-OW2TAMH5.js} +3 -3
  154. package/dist/{tencent-ads-IGD33LO7.js → tencent-ads-XTQZ27YT.js} +3 -3
  155. package/dist/{tencent-docs-C3A4J3CJ.js → tencent-docs-6D6A2VCO.js} +3 -3
  156. package/dist/{tencent-docs-O2SC4FHL.js → tencent-docs-K3TMUIWD.js} +3 -3
  157. package/dist/{tencent-maps-OQOKHVW2.js → tencent-maps-IZYWITJZ.js} +3 -3
  158. package/dist/{tencent-maps-HMMWMNF4.js → tencent-maps-SWI7CLQY.js} +3 -3
  159. package/dist/text-to-cad-B2UP6PKA.js +192 -0
  160. package/dist/text-to-cad-I4B6VBFV.js +193 -0
  161. package/dist/{vercel-KOXDDTHX.js → vercel-CCKRC76D.js} +3 -3
  162. package/dist/{vercel-OLNVDWMG.js → vercel-SBGEMIJJ.js} +3 -3
  163. package/dist/{webflow-OMJKZM54.js → webflow-C3EHNNSN.js} +3 -3
  164. package/dist/{webflow-FULU5Q2I.js → webflow-ZFBJH4CR.js} +3 -3
  165. package/dist/{wechat-miniprogram-skyline-KYCDMQNW.js → wechat-miniprogram-skyline-VNCRERHX.js} +3 -3
  166. package/dist/{wechat-miniprogram-skyline-VR4FVIQL.js → wechat-miniprogram-skyline-Z5JQUV5Q.js} +3 -3
  167. package/dist/{wechat-pay-BCMAJ6UW.js → wechat-pay-RPDKPUEB.js} +3 -3
  168. package/dist/{wechat-pay-YQQKXVUI.js → wechat-pay-XVDGJRF2.js} +3 -3
  169. package/dist/{wonda-NGWIORYN.js → wonda-EL2P44S7.js} +3 -3
  170. package/dist/{wonda-RBABXFNM.js → wonda-XK5JK4X3.js} +3 -3
  171. package/dist/{wordpress-woocommerce-RNA5HB3N.js → wordpress-woocommerce-4MEE5A2M.js} +3 -3
  172. package/dist/{wordpress-woocommerce-RDIUTHYT.js → wordpress-woocommerce-6ECNM2QU.js} +3 -3
  173. package/dist/{wps-LUWHMZQQ.js → wps-E4OZEMOF.js} +3 -3
  174. package/dist/{wps-DAEFQHDE.js → wps-JNQUC4JS.js} +3 -3
  175. package/dist/{yuque-HCHTJWNI.js → yuque-GLAAOS7X.js} +3 -3
  176. package/dist/{yuque-KRH5O74J.js → yuque-MEF6VFLJ.js} +3 -3
  177. package/images/RUNNERS.md +15 -0
  178. package/images/cc-connect-runner/entrypoint.mjs +228 -0
  179. package/images/claude-runner/RUNNER.md +5 -3
  180. package/images/codex-runner/RUNNER.md +5 -3
  181. package/images/gemini-runner/RUNNER.md +5 -2
  182. package/images/hermes-runner/RUNNER.md +4 -2
  183. package/images/hermes-runner/entrypoint.mjs +269 -1
  184. package/images/openclaw-runner/Dockerfile +1 -0
  185. package/images/openclaw-runner/RUNNER.md +3 -0
  186. package/images/openclaw-runner/entrypoint.mjs +249 -1
  187. package/images/openclaw-runner/warm-runtime-deps.mjs +1 -3
  188. package/images/opencode-runner/RUNNER.md +5 -3
  189. package/package.json +3 -3
  190. package/templates/agent-marketplace-buddy.template.json +4 -1
  191. package/templates/bmad-method-buddy.template.json +4 -1
  192. package/templates/code-trainer.template.json +331 -0
  193. package/templates/gstack-buddy.template.json +4 -1
  194. package/templates/little-match-girl.template.json +10 -3
  195. package/dist/console/static/css/index.7f91f806.css +0 -1
  196. package/dist/console/static/js/index.4487e1ff.js +0 -1
  197. package/dist/lark-6LNA3LUQ.js +0 -103
  198. package/dist/lark-URVBZNS4.js +0 -102
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { spawn, spawnSync } from 'node:child_process'
9
+ import { createHash } from 'node:crypto'
9
10
  import {
10
11
  chmodSync,
11
12
  cpSync,
@@ -24,6 +25,11 @@ const CONFIG_MOUNT = process.env.SHADOW_RUNNER_CONFIG_MOUNT ?? '/etc/openclaw'
24
25
  const RUNTIME_FILES_PATH = join(CONFIG_MOUNT, 'runtime-files.json')
25
26
  const RUNTIME_EXTENSIONS_PATH = join(CONFIG_MOUNT, 'runtime-extensions.json')
26
27
  const RUNNER_HOME = process.env.HOME ?? '/home/shadow'
28
+ const HERMES_HOME = process.env.HERMES_HOME ?? join(RUNNER_HOME, '.hermes')
29
+ const TEMPLATE_ROUTINES_PATH =
30
+ process.env.SHADOW_TEMPLATE_ROUTINES_PATH ?? '/etc/shadowob/template-routines.json'
31
+ const HERMES_CRON_STORE_PATH =
32
+ process.env.HERMES_CRON_STORE_PATH ?? join(HERMES_HOME, 'cron', 'jobs.json')
27
33
  const HEALTH_PORT = Number.parseInt(
28
34
  process.env.SHADOW_RUNNER_HEALTH_PORT ?? process.env.OPENCLAW_GATEWAY_PORT ?? '3100',
29
35
  10,
@@ -76,6 +82,267 @@ function modeForPath(path) {
76
82
  return 0o644
77
83
  }
78
84
 
85
+ function isPlainObject(value) {
86
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
87
+ }
88
+
89
+ function stableValue(value) {
90
+ if (Array.isArray(value)) return value.map(stableValue)
91
+ if (!isPlainObject(value)) return value
92
+ return Object.fromEntries(
93
+ Object.entries(value)
94
+ .sort(([left], [right]) => left.localeCompare(right))
95
+ .map(([key, item]) => [key, stableValue(item)]),
96
+ )
97
+ }
98
+
99
+ function stableHash(value) {
100
+ return createHash('sha256')
101
+ .update(JSON.stringify(stableValue(value)))
102
+ .digest('hex')
103
+ }
104
+
105
+ function readJsonFile(path, fallback) {
106
+ if (!existsSync(path)) return fallback
107
+ try {
108
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'))
109
+ return parsed && typeof parsed === 'object' ? parsed : fallback
110
+ } catch (err) {
111
+ console.warn(`[entrypoint] Failed to parse ${path}: ${err.message}`)
112
+ return fallback
113
+ }
114
+ }
115
+
116
+ function parseRoutineEveryMinutes(interval) {
117
+ if (typeof interval !== 'string') return null
118
+ const match = interval.trim().match(/^(\d+)\s*(m|h|d)$/i)
119
+ if (!match) return null
120
+ const amount = Number.parseInt(match[1], 10)
121
+ if (!Number.isFinite(amount) || amount <= 0) return null
122
+ const unit = match[2].toLowerCase()
123
+ return amount * (unit === 'm' ? 1 : unit === 'h' ? 60 : 1440)
124
+ }
125
+
126
+ function isoNow() {
127
+ return new Date().toISOString()
128
+ }
129
+
130
+ function addMinutesIso(minutes) {
131
+ return new Date(Date.now() + minutes * 60_000).toISOString()
132
+ }
133
+
134
+ function buildHermesRoutineSchedule(routine) {
135
+ const schedule = isPlainObject(routine.schedule) ? routine.schedule : {}
136
+ if (typeof schedule.cron === 'string' && schedule.cron.trim()) {
137
+ const expr = schedule.cron.trim()
138
+ return {
139
+ schedule: { kind: 'cron', expr, display: expr },
140
+ scheduleDisplay: expr,
141
+ nextRunAt: null,
142
+ }
143
+ }
144
+ const minutes = parseRoutineEveryMinutes(schedule.interval)
145
+ if (minutes) {
146
+ const display = `every ${minutes}m`
147
+ return {
148
+ schedule: { kind: 'interval', minutes, display },
149
+ scheduleDisplay: display,
150
+ nextRunAt: addMinutesIso(minutes),
151
+ }
152
+ }
153
+ return null
154
+ }
155
+
156
+ function resolveShadowobRoutineDelivery(routine) {
157
+ const deliveries = Array.isArray(routine.deliveries) ? routine.deliveries : []
158
+ for (const delivery of deliveries) {
159
+ if (
160
+ !isPlainObject(delivery) ||
161
+ delivery.pluginId !== 'shadowob' ||
162
+ delivery.kind !== 'channel'
163
+ ) {
164
+ continue
165
+ }
166
+ const target = isPlainObject(delivery.target) ? delivery.target : {}
167
+ const channelEnvKey =
168
+ typeof target.channelEnvKey === 'string' && target.channelEnvKey.trim()
169
+ ? target.channelEnvKey.trim()
170
+ : null
171
+ const channelId =
172
+ (channelEnvKey ? process.env[channelEnvKey] : undefined) ??
173
+ (typeof target.channelId === 'string' ? target.channelId : undefined)
174
+ if (!channelId) continue
175
+ return {
176
+ channelId,
177
+ threadId:
178
+ typeof target.threadId === 'string' && target.threadId.trim()
179
+ ? target.threadId.trim()
180
+ : null,
181
+ }
182
+ }
183
+ return null
184
+ }
185
+
186
+ function managedRoutineJobId(routine) {
187
+ const raw = `shadow-template-${routine.agentId ?? 'agent'}-${routine.id ?? 'routine'}`
188
+ .toLowerCase()
189
+ .replace(/[^a-z0-9._:-]+/g, '-')
190
+ .replace(/^-+|-+$/g, '')
191
+ if (raw.length >= 8 && raw.length <= 64) return raw
192
+ return `shadow-template-${stableHash({ agentId: routine.agentId, id: routine.id }).slice(0, 20)}`
193
+ }
194
+
195
+ function hermesManagedJobShape(job) {
196
+ return {
197
+ id: job.id,
198
+ name: job.name,
199
+ prompt: job.prompt,
200
+ skills: job.skills,
201
+ skill: job.skill,
202
+ model: job.model,
203
+ provider: job.provider,
204
+ base_url: job.base_url,
205
+ script: job.script,
206
+ no_agent: job.no_agent,
207
+ context_from: job.context_from,
208
+ schedule: job.schedule,
209
+ schedule_display: job.schedule_display,
210
+ repeat: job.repeat,
211
+ enabled: job.enabled,
212
+ state: job.state,
213
+ deliver: job.deliver,
214
+ origin: job.origin,
215
+ enabled_toolsets: job.enabled_toolsets,
216
+ workdir: job.workdir,
217
+ profile: job.profile,
218
+ }
219
+ }
220
+
221
+ function buildHermesRoutineJob(routine, now) {
222
+ if (
223
+ !isPlainObject(routine) ||
224
+ typeof routine.id !== 'string' ||
225
+ typeof routine.agentId !== 'string'
226
+ ) {
227
+ return null
228
+ }
229
+ const schedule = buildHermesRoutineSchedule(routine)
230
+ const delivery = resolveShadowobRoutineDelivery(routine)
231
+ if (!schedule || !delivery) return null
232
+
233
+ const origin = {
234
+ platform: 'shadowob',
235
+ chat_id: delivery.channelId,
236
+ ...(delivery.threadId ? { thread_id: delivery.threadId } : {}),
237
+ }
238
+ const job = {
239
+ id: managedRoutineJobId(routine),
240
+ name:
241
+ typeof routine.title === 'string' && routine.title.trim() ? routine.title.trim() : routine.id,
242
+ prompt: String(routine.prompt ?? ''),
243
+ skills: [],
244
+ skill: null,
245
+ model: null,
246
+ provider: null,
247
+ base_url: null,
248
+ script: null,
249
+ no_agent: false,
250
+ context_from: null,
251
+ schedule: schedule.schedule,
252
+ schedule_display: schedule.scheduleDisplay,
253
+ repeat: { times: null, completed: 0 },
254
+ enabled: routine.enabled !== false,
255
+ state: routine.enabled === false ? 'paused' : 'scheduled',
256
+ paused_at: null,
257
+ paused_reason: null,
258
+ created_at: now,
259
+ next_run_at: schedule.nextRunAt,
260
+ last_run_at: null,
261
+ last_status: null,
262
+ last_error: null,
263
+ last_delivery_error: null,
264
+ deliver: delivery.threadId ? 'origin' : `shadowob:${delivery.channelId}`,
265
+ origin,
266
+ enabled_toolsets: null,
267
+ workdir: '/workspace',
268
+ profile: null,
269
+ }
270
+ const managedSpecHash = stableHash(hermesManagedJobShape(job))
271
+ return {
272
+ ...job,
273
+ shadowTemplateRoutine: {
274
+ version: 1,
275
+ routineId: routine.id,
276
+ agentId: routine.agentId,
277
+ sourceHash: typeof routine.sourceHash === 'string' ? routine.sourceHash : null,
278
+ managedSpecHash,
279
+ },
280
+ }
281
+ }
282
+
283
+ function syncTemplateRoutinesToHermesCron() {
284
+ if (!existsSync(TEMPLATE_ROUTINES_PATH)) return
285
+ const seed = readJsonFile(TEMPLATE_ROUTINES_PATH, null)
286
+ const routines = Array.isArray(seed?.routines) ? seed.routines : []
287
+ if (routines.length === 0) return
288
+
289
+ const now = isoNow()
290
+ const store = readJsonFile(HERMES_CRON_STORE_PATH, { jobs: [] })
291
+ const jobs = Array.isArray(store.jobs) ? store.jobs.filter(Boolean) : []
292
+ let changed = false
293
+
294
+ for (const routine of routines) {
295
+ const desired = buildHermesRoutineJob(routine, now)
296
+ if (!desired) {
297
+ console.warn(
298
+ `[entrypoint] Skipping invalid template routine: ${JSON.stringify(routine?.id ?? null)}`,
299
+ )
300
+ continue
301
+ }
302
+ const existingIndex = jobs.findIndex((job) => {
303
+ const marker = isPlainObject(job?.shadowTemplateRoutine) ? job.shadowTemplateRoutine : null
304
+ return marker?.routineId === desired.shadowTemplateRoutine.routineId || job?.id === desired.id
305
+ })
306
+ if (existingIndex < 0) {
307
+ jobs.push(desired)
308
+ changed = true
309
+ console.log(`[entrypoint] Seeded Hermes cron routine: ${desired.id}`)
310
+ continue
311
+ }
312
+ const existing = jobs[existingIndex]
313
+ const marker = isPlainObject(existing.shadowTemplateRoutine)
314
+ ? existing.shadowTemplateRoutine
315
+ : null
316
+ const currentManagedSpecHash = stableHash(hermesManagedJobShape(existing))
317
+ if (!marker || marker.managedSpecHash !== currentManagedSpecHash) {
318
+ console.log(
319
+ `[entrypoint] Preserved user-edited Hermes cron routine: ${existing.id ?? desired.id}`,
320
+ )
321
+ continue
322
+ }
323
+ if (marker.managedSpecHash === desired.shadowTemplateRoutine.managedSpecHash) continue
324
+ jobs[existingIndex] = {
325
+ ...desired,
326
+ id: existing.id ?? desired.id,
327
+ created_at: existing.created_at ?? desired.created_at,
328
+ last_run_at: existing.last_run_at ?? null,
329
+ last_status: existing.last_status ?? null,
330
+ last_error: existing.last_error ?? null,
331
+ last_delivery_error: existing.last_delivery_error ?? null,
332
+ }
333
+ changed = true
334
+ console.log(`[entrypoint] Updated Hermes cron routine from template: ${jobs[existingIndex].id}`)
335
+ }
336
+
337
+ if (!changed) return
338
+ mkdirSync(dirname(HERMES_CRON_STORE_PATH), { recursive: true })
339
+ writeFileSync(HERMES_CRON_STORE_PATH, `${JSON.stringify({ jobs, updated_at: now }, null, 2)}\n`, {
340
+ encoding: 'utf-8',
341
+ mode: 0o600,
342
+ })
343
+ chmodSync(HERMES_CRON_STORE_PATH, 0o600)
344
+ }
345
+
79
346
  function materializeRuntimeFiles() {
80
347
  const files = loadJson(RUNTIME_FILES_PATH, {})
81
348
  for (const [path, content] of Object.entries(files)) {
@@ -226,7 +493,7 @@ function startHermes() {
226
493
  const proc = spawn('hermes', ['gateway'], {
227
494
  env: {
228
495
  ...process.env,
229
- HERMES_HOME: process.env.HERMES_HOME ?? join(RUNNER_HOME, '.hermes'),
496
+ HERMES_HOME,
230
497
  },
231
498
  stdio: ['ignore', 'pipe', 'pipe'],
232
499
  cwd: '/workspace',
@@ -262,6 +529,7 @@ async function main() {
262
529
  materializeRuntimeFiles()
263
530
  materializeCredentialFiles()
264
531
  materializePluginRuntimeAssets()
532
+ syncTemplateRoutinesToHermesCron()
265
533
 
266
534
  if (process.env.SHADOW_RUNNER_VALIDATE_ONLY === '1') {
267
535
  verifyBinary('hermes', ['--version'])
@@ -61,6 +61,7 @@ RUN find node_modules -type d \( -name ".github" -o -name "test" -o -name "tests
61
61
  # TypeScript source changes.
62
62
  WORKDIR /workspace
63
63
  COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.json .npmrc ./
64
+ COPY patches patches
64
65
  COPY packages/shared/package.json packages/shared/package.json
65
66
  COPY packages/sdk/package.json packages/sdk/package.json
66
67
  COPY packages/cli/package.json packages/cli/package.json
@@ -17,6 +17,9 @@ The current image already follows this model more closely than the ACP runners:
17
17
  ShadowOB connector CLI.
18
18
  - `entrypoint.mjs` reads `/etc/openclaw/config.json`.
19
19
  - Runtime extensions are read from `/etc/openclaw/runtime-extensions.json`.
20
+ - Template routines are read from `/etc/shadowob/template-routines.json` and
21
+ synced into the OpenClaw cron store before the gateway starts. The sync keeps a
22
+ managed spec hash so runtime-edited jobs are preserved on redeploy.
20
23
  - The generated OpenClaw config is written outside mutable state by default.
21
24
  - File logging goes to `/var/log/openclaw/entrypoint.log`.
22
25
  - Shadow slash command artifacts are surfaced through
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { spawn, spawnSync } from 'node:child_process'
13
- import { randomBytes } from 'node:crypto'
13
+ import { createHash, randomBytes } from 'node:crypto'
14
14
  import {
15
15
  chmodSync,
16
16
  cpSync,
@@ -33,6 +33,8 @@ const EXTENSIONS_DIR = '/app/extensions'
33
33
  const RUNTIME_FILES_PATH = join(CONFIG_MOUNT, 'runtime-files.json')
34
34
  const RUNTIME_EXTENSIONS_PATH = join(CONFIG_MOUNT, 'runtime-extensions.json')
35
35
  const DEFAULT_SHADOW_SLASH_COMMANDS_PATH = '/etc/shadowob/slash-commands.json'
36
+ const TEMPLATE_ROUTINES_PATH =
37
+ process.env.SHADOW_TEMPLATE_ROUTINES_PATH ?? '/etc/shadowob/template-routines.json'
36
38
  const RUNTIME_CONFIG_DIR = process.env.OPENCLAW_RUNTIME_CONFIG_DIR || '/tmp/openclaw/config'
37
39
  const RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, 'openclaw.json')
38
40
  const OPENCLAW_PACKAGE_DIR = '/app/node_modules/openclaw'
@@ -245,6 +247,251 @@ function materializeCredentialFiles(runtimeExtensions) {
245
247
  }
246
248
  }
247
249
 
250
+ function stableValue(value) {
251
+ if (Array.isArray(value)) return value.map(stableValue)
252
+ if (!isPlainObject(value)) return value
253
+ return Object.fromEntries(
254
+ Object.entries(value)
255
+ .sort(([left], [right]) => left.localeCompare(right))
256
+ .map(([key, item]) => [key, stableValue(item)]),
257
+ )
258
+ }
259
+
260
+ function stableHash(value) {
261
+ return createHash('sha256')
262
+ .update(JSON.stringify(stableValue(value)))
263
+ .digest('hex')
264
+ }
265
+
266
+ function readJsonFile(path, fallback) {
267
+ if (!existsSync(path)) return fallback
268
+ try {
269
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'))
270
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback
271
+ } catch (err) {
272
+ console.warn(`[entrypoint] Failed to parse ${path}: ${err.message}`)
273
+ return fallback
274
+ }
275
+ }
276
+
277
+ function resolveRunnerHomePath(rawPath) {
278
+ if (typeof rawPath !== 'string' || !rawPath.trim()) return null
279
+ const trimmed = rawPath.trim()
280
+ if (trimmed === '~') return RUNNER_HOME
281
+ if (trimmed.startsWith('~/')) return join(RUNNER_HOME, trimmed.slice(2))
282
+ return resolve(trimmed)
283
+ }
284
+
285
+ function resolveOpenClawCronStorePath(openclawConfig) {
286
+ const configured = isPlainObject(openclawConfig.cron) ? openclawConfig.cron.store : undefined
287
+ const candidate =
288
+ resolveRunnerHomePath(configured) ?? join(OPENCLAW_STATE_DIR, 'cron', 'jobs.json')
289
+ const allowedRoots = [RUNNER_HOME, '/home/openclaw']
290
+ if (!allowedRoots.some((root) => candidate === root || candidate.startsWith(`${root}/`))) {
291
+ console.warn(
292
+ `[entrypoint] Skipping template routine sync; cron store outside runner home: ${candidate}`,
293
+ )
294
+ return null
295
+ }
296
+ return candidate
297
+ }
298
+
299
+ function parseRoutineEveryMs(interval) {
300
+ if (typeof interval !== 'string') return null
301
+ const match = interval.trim().match(/^(\d+)\s*(s|m|h|d)$/i)
302
+ if (!match) return null
303
+ const amount = Number.parseInt(match[1], 10)
304
+ if (!Number.isFinite(amount) || amount <= 0) return null
305
+ const unit = match[2].toLowerCase()
306
+ const multiplier =
307
+ unit === 's' ? 1000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000
308
+ return amount * multiplier
309
+ }
310
+
311
+ function buildOpenClawRoutineSchedule(routine) {
312
+ const schedule = isPlainObject(routine.schedule) ? routine.schedule : {}
313
+ if (typeof schedule.cron === 'string' && schedule.cron.trim()) {
314
+ return {
315
+ kind: 'cron',
316
+ expr: schedule.cron.trim(),
317
+ ...(typeof schedule.timezone === 'string' && schedule.timezone.trim()
318
+ ? { tz: schedule.timezone.trim() }
319
+ : {}),
320
+ }
321
+ }
322
+ const everyMs = parseRoutineEveryMs(schedule.interval)
323
+ if (everyMs) return { kind: 'every', everyMs }
324
+ return null
325
+ }
326
+
327
+ function resolveRoutineDeliveryTarget(delivery) {
328
+ if (!isPlainObject(delivery) || delivery.pluginId !== 'shadowob' || delivery.kind !== 'channel') {
329
+ return null
330
+ }
331
+ const target = isPlainObject(delivery.target) ? delivery.target : {}
332
+ const channelEnvKey =
333
+ typeof target.channelEnvKey === 'string' && target.channelEnvKey.trim()
334
+ ? target.channelEnvKey.trim()
335
+ : null
336
+ const channelId =
337
+ (channelEnvKey ? process.env[channelEnvKey] : undefined) ??
338
+ (typeof target.channelId === 'string' ? target.channelId : undefined)
339
+ if (!channelId) return null
340
+ return {
341
+ mode: 'announce',
342
+ channel: 'shadowob',
343
+ to: `shadowob:channel:${channelId}`,
344
+ ...(typeof target.threadId === 'string' && target.threadId.trim()
345
+ ? { threadId: target.threadId.trim() }
346
+ : {}),
347
+ ...(typeof target.accountId === 'string' && target.accountId.trim()
348
+ ? { accountId: target.accountId.trim() }
349
+ : {}),
350
+ bestEffort: true,
351
+ }
352
+ }
353
+
354
+ function managedRoutineJobId(routine) {
355
+ const raw = `shadow-template-${routine.agentId ?? 'agent'}-${routine.id ?? 'routine'}`
356
+ .toLowerCase()
357
+ .replace(/[^a-z0-9._:-]+/g, '-')
358
+ .replace(/^-+|-+$/g, '')
359
+ if (raw.length >= 12 && raw.length <= 96) return raw
360
+ return `shadow-template-${stableHash({ agentId: routine.agentId, id: routine.id }).slice(0, 24)}`
361
+ }
362
+
363
+ function openClawManagedJobShape(job) {
364
+ return {
365
+ agentId: job.agentId,
366
+ sessionKey: job.sessionKey,
367
+ name: job.name,
368
+ description: job.description,
369
+ enabled: job.enabled,
370
+ deleteAfterRun: job.deleteAfterRun,
371
+ schedule: job.schedule,
372
+ sessionTarget: job.sessionTarget,
373
+ wakeMode: job.wakeMode,
374
+ payload: job.payload,
375
+ delivery: job.delivery,
376
+ }
377
+ }
378
+
379
+ function buildOpenClawRoutineJob(routine, now) {
380
+ if (
381
+ !isPlainObject(routine) ||
382
+ typeof routine.id !== 'string' ||
383
+ typeof routine.agentId !== 'string'
384
+ ) {
385
+ return null
386
+ }
387
+ const schedule = buildOpenClawRoutineSchedule(routine)
388
+ if (!schedule) return null
389
+ const delivery = Array.isArray(routine.deliveries)
390
+ ? routine.deliveries.map(resolveRoutineDeliveryTarget).find(Boolean)
391
+ : null
392
+ if (!delivery) return null
393
+ const job = {
394
+ id: managedRoutineJobId(routine),
395
+ agentId: routine.agentId,
396
+ name:
397
+ typeof routine.title === 'string' && routine.title.trim() ? routine.title.trim() : routine.id,
398
+ description:
399
+ typeof routine.description === 'string' && routine.description.trim()
400
+ ? routine.description.trim()
401
+ : `Shadow Cloud template routine ${routine.id}`,
402
+ enabled: routine.enabled !== false,
403
+ createdAtMs: now,
404
+ updatedAtMs: now,
405
+ schedule,
406
+ sessionTarget: 'isolated',
407
+ wakeMode: 'now',
408
+ payload: { kind: 'agentTurn', message: String(routine.prompt ?? '') },
409
+ delivery,
410
+ state: {},
411
+ }
412
+ const managedSpecHash = stableHash(openClawManagedJobShape(job))
413
+ return {
414
+ ...job,
415
+ shadowTemplateRoutine: {
416
+ version: 1,
417
+ routineId: routine.id,
418
+ agentId: routine.agentId,
419
+ sourceHash: typeof routine.sourceHash === 'string' ? routine.sourceHash : null,
420
+ managedSpecHash,
421
+ },
422
+ }
423
+ }
424
+
425
+ function syncTemplateRoutinesToOpenClawCron(openclawConfig) {
426
+ if (!existsSync(TEMPLATE_ROUTINES_PATH)) return
427
+ const seed = readJsonFile(TEMPLATE_ROUTINES_PATH, null)
428
+ const routines = Array.isArray(seed?.routines) ? seed.routines : []
429
+ if (routines.length === 0) return
430
+
431
+ const storePath = resolveOpenClawCronStorePath(openclawConfig)
432
+ if (!storePath) return
433
+
434
+ const now = Date.now()
435
+ const store = readJsonFile(storePath, { version: 1, jobs: [] })
436
+ const jobs = Array.isArray(store.jobs) ? store.jobs.filter(Boolean) : []
437
+ let changed = false
438
+
439
+ for (const routine of routines) {
440
+ const desired = buildOpenClawRoutineJob(routine, now)
441
+ if (!desired) {
442
+ console.warn(
443
+ `[entrypoint] Skipping invalid template routine: ${JSON.stringify(routine?.id ?? null)}`,
444
+ )
445
+ continue
446
+ }
447
+
448
+ const existingIndex = jobs.findIndex((job) => {
449
+ const marker = isPlainObject(job?.shadowTemplateRoutine) ? job.shadowTemplateRoutine : null
450
+ return marker?.routineId === desired.shadowTemplateRoutine.routineId || job?.id === desired.id
451
+ })
452
+
453
+ if (existingIndex < 0) {
454
+ jobs.push(desired)
455
+ changed = true
456
+ console.log(`[entrypoint] Seeded OpenClaw cron routine: ${desired.id}`)
457
+ continue
458
+ }
459
+
460
+ const existing = jobs[existingIndex]
461
+ const marker = isPlainObject(existing.shadowTemplateRoutine)
462
+ ? existing.shadowTemplateRoutine
463
+ : null
464
+ const currentManagedSpecHash = stableHash(openClawManagedJobShape(existing))
465
+ if (!marker || marker.managedSpecHash !== currentManagedSpecHash) {
466
+ console.log(
467
+ `[entrypoint] Preserved user-edited OpenClaw cron routine: ${existing.id ?? desired.id}`,
468
+ )
469
+ continue
470
+ }
471
+
472
+ if (marker.managedSpecHash === desired.shadowTemplateRoutine.managedSpecHash) continue
473
+ jobs[existingIndex] = {
474
+ ...desired,
475
+ id: existing.id ?? desired.id,
476
+ createdAtMs: existing.createdAtMs ?? desired.createdAtMs,
477
+ updatedAtMs: now,
478
+ state: isPlainObject(existing.state) ? existing.state : {},
479
+ }
480
+ changed = true
481
+ console.log(
482
+ `[entrypoint] Updated OpenClaw cron routine from template: ${jobs[existingIndex].id}`,
483
+ )
484
+ }
485
+
486
+ if (!changed) return
487
+ mkdirSync(dirname(storePath), { recursive: true })
488
+ writeFileSync(storePath, `${JSON.stringify({ version: 1, jobs }, null, 2)}\n`, {
489
+ encoding: 'utf-8',
490
+ mode: 0o600,
491
+ })
492
+ chmodSync(storePath, 0o600)
493
+ }
494
+
248
495
  function resolveEnvVars(obj) {
249
496
  if (typeof obj === 'string') {
250
497
  return obj.replace(/\$\{env:([^}]+)\}/g, (_, key) => {
@@ -1061,6 +1308,7 @@ async function main() {
1061
1308
  }
1062
1309
  writeFileSync(configPath, JSON.stringify(openclawConfig, null, 2), 'utf-8')
1063
1310
  console.log('[entrypoint] Runtime config restored after setup')
1311
+ syncTemplateRoutinesToOpenClawCron(openclawConfig)
1064
1312
 
1065
1313
  // 2e. Overlay workspace files from ConfigMap (SOUL.md, AGENTS.md, etc.)
1066
1314
  // These are agent-specific files generated by the cloud config builder that
@@ -63,9 +63,7 @@ async function main() {
63
63
  })
64
64
 
65
65
  if (scan.conflicts.length > 0) {
66
- console.warn(
67
- `[runtime-deps] ${scan.conflicts.length} bundled dependency conflict(s) detected`,
68
- )
66
+ console.warn(`[runtime-deps] ${scan.conflicts.length} bundled dependency conflict(s) detected`)
69
67
  }
70
68
 
71
69
  if (scan.missing.length === 0) {
@@ -145,9 +145,11 @@ type = "shadowob"
145
145
  - Skills: materialize `.opencode/skills` first; optionally also emit
146
146
  `.agents/skills` if a workflow must be shared with Codex.
147
147
  - MCP: write the OpenCode `mcp` object, not OpenClaw `mcp`.
148
- - Cron/routine: OpenCode does not provide the same native cron surface as
149
- OpenClaw or Hermes in the researched docs; Cloud should own scheduling for
150
- phase 1.
148
+ - Cron/routine: Cloud template routines are materialized by the shared
149
+ cc-connect runner from `/etc/shadowob/template-routines.json` into
150
+ `~/.cc-connect/crons/jobs.json`. Managed jobs use deterministic ids and a
151
+ spec hash so user-edited schedules are preserved. ShadowOB delivery uses
152
+ `session_key = "shadowob:channel:<channel_id>"` or a thread-qualified variant.
151
153
  - Hooks: prefer OpenCode plugins for deterministic lifecycle behavior.
152
154
  - Subagents: use OpenCode `agent` and command `subtask` config instead of
153
155
  OpenClaw multi-agent routing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadowob/cloud",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "shadowob-cloud — deploy AI agents to Kubernetes",
5
5
  "type": "module",
6
6
  "bin": {
@@ -58,8 +58,8 @@
58
58
  "yaml": "^2.8.4",
59
59
  "zod": "^3.25.76",
60
60
  "zustand": "^5.0.13",
61
- "@shadowob/sdk": "1.1.7",
62
- "@shadowob/shared": "1.1.7"
61
+ "@shadowob/sdk": "1.1.9",
62
+ "@shadowob/shared": "1.1.9"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@playwright/test": "^1.52.0",
@@ -49,7 +49,10 @@
49
49
  "channels": ["choose", "build", "review"],
50
50
  "agentId": "agent-marketplace-buddy"
51
51
  }
52
- ]
52
+ ],
53
+ "greeting": {
54
+ "entryChannelId": "choose"
55
+ }
53
56
  }
54
57
  }
55
58
  ],
@@ -49,7 +49,10 @@
49
49
  "channels": ["analysis", "planning", "delivery"],
50
50
  "agentId": "bmad-buddy"
51
51
  }
52
- ]
52
+ ],
53
+ "greeting": {
54
+ "entryChannelId": "delivery"
55
+ }
53
56
  }
54
57
  }
55
58
  ],