failproofai 0.0.14 → 0.0.15-beta.0

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 (271) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +6 -6
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +2 -2
  5. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  13. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  14. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  23. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/audit/invite/route.js +1 -1
  31. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/audit/run/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
  35. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +1 -1
  37. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/logout/route.js +1 -1
  39. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +1 -1
  41. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/auth/status/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  45. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
  47. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  49. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/index.html +1 -1
  52. package/.next/standalone/.next/server/app/index.rsc +15 -15
  53. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  54. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  55. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  56. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  57. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  58. package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
  59. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  60. package/.next/standalone/.next/server/app/page.js +2 -2
  61. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
  64. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  65. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  66. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
  69. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  70. package/.next/standalone/.next/server/app/project/[name]/page.js +4 -4
  71. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  72. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  73. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
  74. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  75. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  76. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +4 -4
  77. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  78. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  79. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
  80. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  81. package/.next/standalone/.next/server/app/projects/page.js +3 -3
  82. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0hv08mx._.js → [root-of-the-server]__0-dh5jy._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0obaznk._.js → [root-of-the-server]__0h3lfmp._.js} +1 -1
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0xvsnvt._.js → [root-of-the-server]__0l4vuqm._.js} +1 -1
  91. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0longh-._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0sb_5m8._.js +4 -4
  93. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +6 -4
  94. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1hkdkmz._.js → [root-of-the-server]__10oa-v9._.js} +1 -1
  95. package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
  96. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1i9rknc._.js → [root-of-the-server]__18d-9oi._.js} +1 -1
  97. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +3 -0
  98. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1is3glr._.js +3 -0
  100. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  101. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +6 -6
  102. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0hdsupo._.js → [root-of-the-server]__0114ewg._.js} +3 -3
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01od0m6._.js +3 -0
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +4 -0
  108. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1s2b6h0._.js → [root-of-the-server]__04o-kw7._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +4 -0
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +4 -0
  111. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lpvwqm._.js +21 -0
  112. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ynf7tx._.js → [root-of-the-server]__0qucxyj._.js} +3 -3
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +4 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0uno39i._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +8 -8
  116. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__00cz9as._.js → [root-of-the-server]__1apb7k_._.js} +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +4 -0
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1i21qhv._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1py795m._.js +3 -0
  121. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +3 -0
  122. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +4 -0
  123. package/.next/standalone/.next/server/chunks/ssr/_077y5np._.js +3 -0
  124. package/.next/standalone/.next/server/chunks/ssr/_1-3unkg._.js +3 -0
  125. package/.next/standalone/.next/server/chunks/ssr/_1e19atl._.js +3 -0
  126. package/.next/standalone/.next/server/chunks/ssr/_1m_p7pi._.js +3 -0
  127. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/app_audit_loading_tsx_1j1kc6j._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/app_components_project-list_tsx_1ke6anr._.js +3 -0
  130. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  132. package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +1 -1
  135. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0gkzjaa.js → node_modules_html-to-image_es_index_0w82veb.js} +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
  137. package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
  138. package/.next/standalone/.next/server/pages/404.html +1 -1
  139. package/.next/standalone/.next/server/pages/500.html +1 -1
  140. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  141. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  142. package/.next/standalone/.next/static/chunks/0hn30_j22wpp_.js +6 -0
  143. package/.next/standalone/.next/static/chunks/{3s8aaig91937v.js → 1_20f96f1253b.js} +1 -1
  144. package/.next/standalone/.next/static/chunks/1hdhteui932bq.js +1 -0
  145. package/.next/standalone/.next/static/chunks/{33u59vf_8xpd-.js → 1j2xm011_aoae.js} +1 -1
  146. package/.next/standalone/.next/static/chunks/1qzsbft-jtnf8.js +1 -0
  147. package/.next/standalone/.next/static/chunks/{3-ogr79xqbw0h.js → 2mazwb1qmur1p.js} +1 -1
  148. package/.next/standalone/.next/static/chunks/2pujpnfx4pws6.css +2 -0
  149. package/.next/standalone/.next/static/chunks/2xbkqlma_-61k.js +1 -0
  150. package/.next/standalone/.next/static/chunks/{29o4j32z643_a.js → 2y7acsw0--8xf.js} +1 -1
  151. package/.next/standalone/.next/static/chunks/3060a-0fayz96.js +1 -0
  152. package/.next/standalone/.next/static/chunks/312ihvhxeuoi_.js +1 -0
  153. package/.next/standalone/.next/static/chunks/31c084gsop1bb.js +1 -0
  154. package/.next/standalone/.next/static/chunks/{1ua5armwfph8o.js → 31fmsp1soxi07.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{02jcotghcxrbu.js → 3kmwfieo15mr2.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{2uh_4k4m296z5.js → 3q9y67-fobgpc.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{turbopack-10hj8t5pf33tu.js → turbopack-1lsv92x7klwrn.js} +1 -1
  158. package/.next/standalone/app/actions/get-hooks-config.ts +85 -0
  159. package/.next/standalone/app/components/project-list.tsx +229 -42
  160. package/.next/standalone/app/policies/hooks-client.tsx +112 -14
  161. package/.next/standalone/app/project/[name]/page.tsx +12 -2
  162. package/.next/standalone/integration-suite/README.md +43 -1
  163. package/.next/standalone/integration-suite/ci-entrypoint.sh +21 -6
  164. package/.next/standalone/integration-suite/install-clis.sh +58 -13
  165. package/.next/standalone/integration-suite/probe-cli.sh +51 -11
  166. package/.next/standalone/integration-suite/report.js +52 -6
  167. package/.next/standalone/integration-suite/run.sh +55 -3
  168. package/.next/standalone/lib/hermes-profiles.ts +156 -0
  169. package/.next/standalone/lib/hermes-projects.ts +91 -25
  170. package/.next/standalone/lib/hermes-sessions.ts +65 -10
  171. package/.next/standalone/lib/openclaw-projects.ts +111 -18
  172. package/.next/standalone/lib/openclaw-sessions.ts +20 -0
  173. package/.next/standalone/lib/paths.ts +36 -0
  174. package/.next/standalone/lib/project-tree.ts +193 -0
  175. package/.next/standalone/lib/projects.ts +22 -0
  176. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  177. package/.next/standalone/node_modules/next/dist/build/next-config-ts/transpile-config.js +3 -114
  178. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  179. package/.next/standalone/node_modules/next/dist/cli/next-test.js +2 -1
  180. package/.next/standalone/node_modules/next/dist/compiled/http-proxy/index.js +5 -5
  181. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +11 -10
  182. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +11 -10
  183. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +2 -2
  184. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +9 -9
  185. package/.next/standalone/node_modules/next/dist/lib/install-dependencies.js +1 -1
  186. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  187. package/.next/standalone/node_modules/next/dist/lib/typescript/loadTsConfig.js +181 -0
  188. package/.next/standalone/node_modules/next/dist/lib/typescript/missingDependencyError.js +1 -1
  189. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheckCli.js +53 -0
  190. package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +256 -0
  191. package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +97 -56
  192. package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +129 -34
  193. package/.next/standalone/node_modules/next/dist/server/app-render/manifests-singleton.js +52 -4
  194. package/.next/standalone/node_modules/next/dist/server/config-schema.js +1 -1
  195. package/.next/standalone/node_modules/next/dist/server/config-shared.js +1 -2
  196. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  197. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  198. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  199. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +8 -27
  200. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  201. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +90 -53
  202. package/.next/standalone/node_modules/next/dist/server/lib/patch-fetch.js +15 -2
  203. package/.next/standalone/node_modules/next/dist/server/lib/router-utils/proxy-request.js +4 -0
  204. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  205. package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-cache.js +1 -1
  206. package/.next/standalone/node_modules/next/dist/shared/lib/errors/canary-only-config-error.js +1 -1
  207. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/prepare-destination.js +2 -1
  208. package/.next/standalone/node_modules/next/dist/shared/lib/server-reference-info.js +12 -0
  209. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  210. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  211. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  212. package/.next/standalone/node_modules/next/package.json +10 -10
  213. package/.next/standalone/package.json +7 -6
  214. package/.next/standalone/pi-extension/index.ts +38 -5
  215. package/.next/standalone/server.js +1 -1
  216. package/dist/cli.mjs +623 -308
  217. package/lib/hermes-profiles.ts +156 -0
  218. package/lib/hermes-projects.ts +91 -25
  219. package/lib/hermes-sessions.ts +65 -10
  220. package/lib/openclaw-projects.ts +111 -18
  221. package/lib/openclaw-sessions.ts +20 -0
  222. package/lib/paths.ts +36 -0
  223. package/lib/project-tree.ts +193 -0
  224. package/lib/projects.ts +22 -0
  225. package/package.json +7 -6
  226. package/pi-extension/index.ts +38 -5
  227. package/scripts/launch.ts +7 -0
  228. package/src/audit/cli-adapters/hermes.ts +6 -3
  229. package/src/audit/cli-adapters/openclaw.ts +17 -13
  230. package/src/hooks/configure-wizard.ts +24 -2
  231. package/src/hooks/custom-hooks-loader.ts +58 -4
  232. package/src/hooks/enforcement-capability.ts +266 -0
  233. package/src/hooks/handler.ts +12 -1
  234. package/src/hooks/hook-activity-store.ts +14 -0
  235. package/src/hooks/hooks-config.ts +59 -1
  236. package/src/hooks/integrations.ts +123 -19
  237. package/src/hooks/manager.ts +93 -27
  238. package/src/hooks/policy-evaluator.ts +59 -7
  239. package/src/hooks/policy-types.ts +26 -0
  240. package/src/hooks/types.ts +50 -2
  241. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +0 -3
  243. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +0 -3
  244. package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +0 -3
  245. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +0 -4
  246. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +0 -3
  247. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +0 -4
  248. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +0 -3
  249. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +0 -3
  250. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +0 -4
  251. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +0 -4
  252. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zf8xlq._.js +0 -3
  253. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +0 -4
  254. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +0 -3
  255. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fax1sl._.js +0 -19
  256. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +0 -4
  257. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +0 -3
  258. package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +0 -3
  259. package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +0 -3
  260. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +0 -3
  261. package/.next/standalone/.next/static/chunks/06z_ex1ts3n4s.js +0 -1
  262. package/.next/standalone/.next/static/chunks/0o0ut0ugd0_-s.js +0 -1
  263. package/.next/standalone/.next/static/chunks/0xl97x-874k-u.js +0 -1
  264. package/.next/standalone/.next/static/chunks/19d7hqx8udopz.js +0 -1
  265. package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +0 -2
  266. package/.next/standalone/.next/static/chunks/370sj2lgkw5ey.js +0 -1
  267. package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +0 -1
  268. package/.next/standalone/.next/static/chunks/42oeva1-w0z2d.js +0 -6
  269. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_buildManifest.js +0 -0
  270. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_clientMiddlewareManifest.js +0 -0
  271. /package/.next/standalone/.next/static/{WQRKgSfFekLcC_pvyzp4S → QFmU1xD4R9N_HVfwccP6A}/_ssgManifest.js +0 -0
@@ -1,20 +1,32 @@
1
1
  /**
2
2
  * Hermes (hermes-agent) session enumeration — AUDIT-ONLY.
3
3
  *
4
- * Reads the `sessions` table directly from `~/.hermes/state.db` via the bundled
5
- * sql.js reader. All gateway users' sessions live in the one DB → "audit
6
- * everyone from one place". Because we query the table (not a text `sessions
7
- * list`), we get `source`/`cwd`/`message_count` up front — enough to group by
8
- * channel and to build a real per-transcript cache key.
4
+ * Reads the `sessions` table directly from every profile's `state.db` via the
5
+ * bundled sql.js reader. All of a profile's gateway users live in its one DB →
6
+ * "audit everyone from one place". Because we query the table (not a text
7
+ * `sessions list`), we get `source`/`cwd`/`message_count` up front — enough to
8
+ * group and to build a real per-transcript cache key.
9
+ *
10
+ * Sessions are grouped by (profile, source): a profile is a whole separate
11
+ * Hermes home (see lib/hermes-profiles.ts), so it is the outer scope, and
12
+ * `source` (slack/telegram/cli/cron) splits the channels within it.
9
13
  */
10
14
  import { openSqliteReadonly } from "./sqlite-reader";
11
- import { hermesDbPath, epochToMs } from "./hermes-sessions";
15
+ import { hermesDbPaths, epochToMs } from "./hermes-sessions";
16
+ import {
17
+ HERMES_DEFAULT_PROFILE,
18
+ hermesProjectName,
19
+ hermesProjectPath,
20
+ hermesProjectNameCandidates,
21
+ } from "./hermes-profiles";
12
22
  import { runtimeCache } from "./runtime-cache";
13
23
  import type { ProjectFolder, SessionFile } from "./projects";
14
24
  import { formatDate } from "./format-date";
15
25
 
16
26
  export interface HermesSessionRef {
17
27
  sessionId: string;
28
+ /** Which profile's state.db this row came from (`"default"` for `~/.hermes`). */
29
+ profile: string;
18
30
  source?: string;
19
31
  cwd?: string;
20
32
  title?: string;
@@ -52,11 +64,24 @@ interface SessionRow {
52
64
  }
53
65
 
54
66
  /**
55
- * List every Hermes session (all users). Returns `[]` when the DB is missing or
56
- * unreadable (fail-open — the audit just skips Hermes).
67
+ * List every Hermes session — every user, of every profile. A missing or
68
+ * unreadable profile DB is skipped rather than failing the batch (fail-open:
69
+ * the audit degrades to the profiles it can read instead of dropping Hermes).
57
70
  */
58
71
  export async function getHermesSessions(): Promise<HermesSessionRef[]> {
59
- const db = await openSqliteReadonly(hermesDbPath());
72
+ const out: HermesSessionRef[] = [];
73
+ for (const { profile, dbPath } of hermesDbPaths()) {
74
+ out.push(...(await readSessionsFromDb(dbPath, profile)));
75
+ }
76
+ out.sort((a, b) => b.mtimeMs - a.mtimeMs);
77
+ return out;
78
+ }
79
+
80
+ async function readSessionsFromDb(
81
+ dbPath: string,
82
+ profile: string,
83
+ ): Promise<HermesSessionRef[]> {
84
+ const db = await openSqliteReadonly(dbPath);
60
85
  if (!db) return [];
61
86
  try {
62
87
  const rows = db.query<SessionRow>(
@@ -69,6 +94,7 @@ export async function getHermesSessions(): Promise<HermesSessionRef[]> {
69
94
  );
70
95
  return rows.map((r) => ({
71
96
  sessionId: r.id,
97
+ profile,
72
98
  source: r.source ?? undefined,
73
99
  cwd: r.cwd ?? undefined,
74
100
  title: r.title ?? undefined,
@@ -92,29 +118,44 @@ export const getCachedHermesSessions = runtimeCache(getHermesSessions, 2);
92
118
  // ── Dashboard history browser (projects list + project-detail sessions) ──
93
119
 
94
120
  /**
95
- * Surface Hermes gateway sessions as synthetic "projects" grouped by `source`
96
- * (slack/telegram/cli/cron) — gateway sessions have no cwd to group by. One
97
- * ProjectFolder per source; its `name` is `hermes-<source>`, reversed in
98
- * `getHermesSessionsByEncodedName`.
121
+ * Surface Hermes gateway sessions as synthetic "projects" grouped by
122
+ * (profile, source) — gateway sessions have no cwd to group by. One
123
+ * ProjectFolder per pair, named by `hermesProjectName`.
99
124
  */
100
125
  export async function getHermesProjects(): Promise<ProjectFolder[]> {
101
126
  const sessions = await getHermesSessions();
102
- const latestBySource = new Map<string, number>();
127
+ // key → { profile, source, latest }; keeps profile/source intact for naming
128
+ // (the key itself is not parseable back when a profile name contains "-").
129
+ const groups = new Map<
130
+ string,
131
+ { profile: string; source: string; latest: number; count: number }
132
+ >();
103
133
  for (const s of sessions) {
104
134
  if (s.messageCount <= 0 && !s.hasMessages) continue; // skip empty (message_count can lag; trust real messages)
105
- const src = s.source ?? "unknown";
106
- latestBySource.set(src, Math.max(latestBySource.get(src) ?? 0, s.mtimeMs));
135
+ const profile = s.profile || HERMES_DEFAULT_PROFILE;
136
+ const source = s.source ?? "unknown";
137
+ const key = JSON.stringify([profile, source]);
138
+ const prev = groups.get(key);
139
+ if (prev) {
140
+ prev.latest = Math.max(prev.latest, s.mtimeMs);
141
+ // Sessions, not messages — `messageCount` counts messages within one
142
+ // session and is documented as possibly lagging.
143
+ prev.count += 1;
144
+ } else {
145
+ groups.set(key, { profile, source, latest: s.mtimeMs, count: 1 });
146
+ }
107
147
  }
108
148
  const out: ProjectFolder[] = [];
109
- for (const [src, latest] of latestBySource) {
149
+ for (const { profile, source, latest, count } of groups.values()) {
110
150
  const lastModified = new Date(latest);
111
151
  out.push({
112
- name: `hermes-${src}`,
113
- path: `hermes:${src}`,
152
+ name: hermesProjectName(profile, source),
153
+ path: hermesProjectPath(profile, source),
114
154
  isDirectory: true,
115
155
  lastModified,
116
156
  lastModifiedFormatted: formatDate(lastModified),
117
157
  cli: ["hermes"],
158
+ sessionCount: count,
118
159
  });
119
160
  }
120
161
  out.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
@@ -127,18 +168,43 @@ export interface HermesProjectByName {
127
168
  }
128
169
 
129
170
  /**
130
- * Resolve the Hermes sessions for a synthetic project name (`hermes-<source>`),
131
- * for the project-detail page. Non-Hermes names resolve to empty.
171
+ * Resolve the Hermes sessions for a synthetic project name
172
+ * (`hermes-<profile>-<source>`), for the project-detail page. Non-Hermes names
173
+ * resolve to empty.
132
174
  */
133
175
  export async function getHermesSessionsByEncodedName(
134
176
  name: string,
135
177
  ): Promise<HermesProjectByName> {
136
- if (!name.startsWith("hermes-")) return { cwd: null, sessions: [] };
137
- const source = name.slice("hermes-".length);
178
+ const candidates = hermesProjectNameCandidates(name);
179
+ if (candidates.length === 0) return { cwd: null, sessions: [] };
180
+
138
181
  const sessions = await getHermesSessions();
139
- const matched = sessions.filter((s) => (s.messageCount > 0 || s.hasMessages) && (s.source ?? "unknown") === source);
182
+ const matching = (split: { profile: string; source: string }) =>
183
+ sessions.filter(
184
+ (s) =>
185
+ (s.messageCount > 0 || s.hasMessages) &&
186
+ (s.profile || HERMES_DEFAULT_PROFILE) === split.profile &&
187
+ (s.source ?? "unknown") === split.source,
188
+ );
189
+
190
+ // First split that actually owns sessions — longest-match alone picks the
191
+ // wrong profile when one name is a hyphen-prefix of another. Falling back to
192
+ // the best guess keeps the page labelled sensibly when nothing matches.
193
+ let chosen = candidates[0];
194
+ let matched = matching(chosen);
195
+ if (matched.length === 0) {
196
+ for (const candidate of candidates.slice(1)) {
197
+ const hits = matching(candidate);
198
+ if (hits.length > 0) {
199
+ chosen = candidate;
200
+ matched = hits;
201
+ break;
202
+ }
203
+ }
204
+ }
205
+ const { profile, source } = chosen;
140
206
  return {
141
- cwd: `hermes:${source}`,
207
+ cwd: hermesProjectPath(profile, source),
142
208
  sessions: matched.map((s) => {
143
209
  const lastModified = new Date(s.mtimeMs);
144
210
  return {
@@ -12,12 +12,22 @@
12
12
  * `hermesRowsToLogEntries` pairs them (mirrors `lib/codex-sessions.ts`) and is a
13
13
  * PURE function of the message rows, so it is unit-testable without a DB.
14
14
  *
15
+ * PROFILES: Hermes profiles are separate home dirs, each with its OWN state.db
16
+ * (`~/.hermes/state.db` for the default, `~/.hermes/profiles/<name>/state.db`
17
+ * otherwise) — see lib/hermes-profiles.ts. So every read here fans out across
18
+ * profiles instead of assuming one DB.
19
+ *
15
20
  * DB path override: set `HERMES_DB_PATH` (used by tests and to point at a copied
16
- * or remote state.db).
21
+ * or remote state.db) — it collapses discovery to that single file. Set
22
+ * `HERMES_HOME` instead to audit a real multi-profile tree.
17
23
  */
18
- import { homedir } from "node:os";
19
24
  import { join } from "node:path";
20
25
  import { openSqliteReadonly } from "./sqlite-reader";
26
+ import {
27
+ listHermesProfiles,
28
+ hermesProjectPath,
29
+ HERMES_DEFAULT_PROFILE,
30
+ } from "./hermes-profiles";
21
31
  import { runtimeCache } from "./runtime-cache";
22
32
  import {
23
33
  baseEntry,
@@ -32,9 +42,26 @@ import {
32
42
  } from "./log-entries";
33
43
  import { formatDuration } from "./format-duration";
34
44
 
35
- /** Absolute path to Hermes's SQLite DB (override with HERMES_DB_PATH). */
36
- export function hermesDbPath(): string {
37
- return process.env.HERMES_DB_PATH || join(homedir(), ".hermes", "state.db");
45
+ /** One Hermes state.db, tagged with the profile that owns it. */
46
+ export interface HermesDb {
47
+ profile: string;
48
+ dbPath: string;
49
+ }
50
+
51
+ /**
52
+ * Every Hermes state.db to read — one per profile on disk.
53
+ *
54
+ * `HERMES_DB_PATH` still wins and collapses this to a single synthetic
55
+ * `default` profile, so pointing at a copied/remote state.db keeps working
56
+ * exactly as before.
57
+ */
58
+ export function hermesDbPaths(): HermesDb[] {
59
+ const override = process.env.HERMES_DB_PATH;
60
+ if (override) return [{ profile: HERMES_DEFAULT_PROFILE, dbPath: override }];
61
+ return listHermesProfiles().map((p) => ({
62
+ profile: p.name,
63
+ dbPath: join(p.home, "state.db"),
64
+ }));
38
65
  }
39
66
 
40
67
  /** Coerce a Hermes epoch value (seconds or ms) to epoch ms. Hermes stores
@@ -210,6 +237,8 @@ export interface HermesSessionLogData {
210
237
  rawLines: Record<string, unknown>[];
211
238
  cwd?: string;
212
239
  filePath: string; // synthetic — hermes keeps sessions in a DB; we use hermes://<id>
240
+ /** Which profile's state.db the session was found in. */
241
+ profile: string;
213
242
  }
214
243
 
215
244
  interface HermesMessageRow {
@@ -223,14 +252,33 @@ interface HermesMessageRow {
223
252
  }
224
253
 
225
254
  /**
226
- * Load one session by ID from `state.db`. Returns `null` when the DB is
227
- * unavailable or the session doesn't exist.
255
+ * Load one session by ID, searching every profile's `state.db` and returning the
256
+ * first hit. Returns `null` when no profile has it.
257
+ *
258
+ * Searching beats encoding the profile into the path: session ids are
259
+ * `YYYYMMDD_HHMMSS_<hash>`, so a cross-profile collision is vanishingly
260
+ * unlikely, and `hermes://<id>` therefore stays a stable identifier — no
261
+ * migration for transcript paths already recorded in audit history, and no
262
+ * change to the download route or its id validator. Profile order from
263
+ * `hermesDbPaths()` is deterministic, so the tie-break is too.
228
264
  */
229
265
  export async function getHermesSessionLog(
230
266
  sessionId: string,
231
267
  ): Promise<HermesSessionLogData | null> {
232
268
  if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return null;
233
- const db = await openSqliteReadonly(hermesDbPath());
269
+ for (const { profile, dbPath } of hermesDbPaths()) {
270
+ const found = await loadFromDb(dbPath, profile, sessionId);
271
+ if (found) return found;
272
+ }
273
+ return null;
274
+ }
275
+
276
+ async function loadFromDb(
277
+ dbPath: string,
278
+ profile: string,
279
+ sessionId: string,
280
+ ): Promise<HermesSessionLogData | null> {
281
+ const db = await openSqliteReadonly(dbPath);
234
282
  if (!db) return null;
235
283
  try {
236
284
  const sessionRows = db.query<{ source: string | null; cwd: string | null }>(
@@ -250,14 +298,21 @@ export async function getHermesSessionLog(
250
298
  msgRows as unknown as Record<string, unknown>[],
251
299
  "session",
252
300
  );
253
- // Gateway sessions have no cwd → group by source (slack/telegram/cli/cron).
301
+ // Gateway sessions have no cwd → fall back to the same (profile, source)
302
+ // key the projects panel and the audit adapter group by, so an event's cwd
303
+ // and its projectName agree.
254
304
  const cwd =
255
- realCwd && realCwd.length > 0 ? realCwd : source ? `hermes:${source}` : undefined;
305
+ realCwd && realCwd.length > 0
306
+ ? realCwd
307
+ : source
308
+ ? hermesProjectPath(profile, source)
309
+ : undefined;
256
310
  return {
257
311
  entries,
258
312
  rawLines: msgRows as unknown as Record<string, unknown>[],
259
313
  cwd,
260
314
  filePath: `hermes://${sessionId}`,
315
+ profile,
261
316
  };
262
317
  } catch {
263
318
  return null;
@@ -2,7 +2,7 @@
2
2
  * OpenClaw (openclaw gateway) session enumeration — AUDIT-ONLY.
3
3
  *
4
4
  * Surfaces the on-disk transcripts (agents/<agentId>/sessions/<uuid>.jsonl) as
5
- * synthetic dashboard "projects" grouped by agentId. The per-agent
5
+ * synthetic dashboard "projects" grouped by (agentId, channel). The per-agent
6
6
  * `sessions.json` index maps sessionKey → {sessionId, timestamps}; we read it to
7
7
  * recover the sessionKey (which encodes the channel for gateway sessions) and a
8
8
  * reliable last-activity time. Verified live against openclaw v2026.7.1.
@@ -10,7 +10,7 @@
10
10
  import { readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { runtimeCache } from "./runtime-cache";
13
- import { listOpenClawTranscripts, openclawHome } from "./openclaw-sessions";
13
+ import { listOpenClawAgents, listOpenClawTranscripts, openclawHome } from "./openclaw-sessions";
14
14
  import type { ProjectFolder, SessionFile } from "./projects";
15
15
  import { formatDate } from "./format-date";
16
16
 
@@ -26,6 +26,8 @@ export interface OpenClawSessionRef {
26
26
  * gateway-metadata columns. */
27
27
  chatId?: string;
28
28
  chatType?: string;
29
+ /** Absolute path to the `<uuid>.jsonl` transcript this ref came from. */
30
+ transcriptPath: string;
29
31
  mtimeMs: number;
30
32
  sizeBytes: number;
31
33
  }
@@ -100,6 +102,7 @@ export async function getOpenClawSessions(): Promise<OpenClawSessionRef[]> {
100
102
  label: meta?.label,
101
103
  chatType: meta?.chatType,
102
104
  chatId: meta?.chatId,
105
+ transcriptPath: t.transcriptPath,
103
106
  mtimeMs: meta?.lastMs ?? t.mtimeMs,
104
107
  sizeBytes: t.sizeBytes,
105
108
  });
@@ -112,28 +115,95 @@ export const getCachedOpenClawSessions = runtimeCache(getOpenClawSessions, 2);
112
115
 
113
116
  // ── Dashboard history browser (projects list + project-detail sessions) ──
114
117
 
118
+ /** Encoded project name for an (agent, channel) pair. */
119
+ export function openClawProjectName(agentId: string, channel: string): string {
120
+ return `openclaw-${agentId}-${channel}`;
121
+ }
122
+
123
+ /** Machine-readable grouping key, shown under the project name. */
124
+ export function openClawProjectPath(agentId: string, channel: string): string {
125
+ return `openclaw:${agentId}:${channel}`;
126
+ }
127
+
128
+ export interface OpenClawNameSplit {
129
+ agentId: string | null;
130
+ channel: string;
131
+ }
132
+
115
133
  /**
116
- * Surface OpenClaw sessions as synthetic "projects" grouped by **channel**
117
- * (telegram/slack/…/local) — gateway sessions run per channel, not in a host
118
- * repo. Mirrors Hermes's group-by-source. One ProjectFolder per channel; its
119
- * `name` is `openclaw-<channel>`, reversed in `getOpenClawSessionsByEncodedName`.
134
+ * Every way `openclaw-<agentId>-<channel>` could split, best guess first.
135
+ *
136
+ * Both an agentId and a channel may contain `-`, so the slug is never split
137
+ * blindly: each candidate is an agent id that actually exists on disk, tried
138
+ * longest-first. That alone is not sufficient — with agents `main` and
139
+ * `main-bot` on disk, `openclaw-main-bot-telegram` could be `main-bot`+`telegram`
140
+ * OR `main`+`bot-telegram`, and length alone picks the wrong one whenever the
141
+ * shorter agent owns it. So callers walk the candidates and take the first that
142
+ * matches real sessions.
143
+ *
144
+ * The last candidate is always the legacy channel-only `openclaw-<channel>`
145
+ * form (`agentId: null`), so links shared before agents became part of the name
146
+ * keep resolving — to every agent on that channel, which is what they meant.
147
+ */
148
+ export function openClawProjectNameCandidates(name: string): OpenClawNameSplit[] {
149
+ if (!name.startsWith("openclaw-")) return [];
150
+ const rest = name.slice("openclaw-".length);
151
+ if (!rest) return [];
152
+
153
+ const out: OpenClawNameSplit[] = [];
154
+ const agents = listOpenClawAgents().sort((a, b) => b.length - a.length);
155
+ for (const agentId of agents) {
156
+ const prefix = `${agentId}-`;
157
+ if (rest.startsWith(prefix) && rest.length > prefix.length) {
158
+ out.push({ agentId, channel: rest.slice(prefix.length) });
159
+ }
160
+ }
161
+ out.push({ agentId: null, channel: rest });
162
+ return out;
163
+ }
164
+
165
+ /** The best-guess split for a project name — `null` if it isn't an OpenClaw one. */
166
+ export function parseOpenClawProjectName(name: string): OpenClawNameSplit | null {
167
+ return openClawProjectNameCandidates(name)[0] ?? null;
168
+ }
169
+
170
+ /**
171
+ * Surface OpenClaw sessions as synthetic "projects" grouped by **(agent,
172
+ * channel)** — gateway sessions have no host repo to group by.
173
+ *
174
+ * Agent is the OUTER axis because it is the stable one: `agentId` is a
175
+ * directory name, whereas `channel` is derived from the LAST channel a session
176
+ * used. Grouping by channel alone (as this did before) also collapsed two
177
+ * different agents that both talk on Telegram into one row with one mixed
178
+ * session list — and disagreed with the audit adapter, which grouped by agent.
120
179
  */
121
180
  export async function getOpenClawProjects(): Promise<ProjectFolder[]> {
122
181
  const sessions = await getOpenClawSessions();
123
- const latestByChannel = new Map<string, number>();
182
+ const groups = new Map<
183
+ string,
184
+ { agentId: string; channel: string; latest: number; count: number }
185
+ >();
124
186
  for (const s of sessions) {
125
- latestByChannel.set(s.channel, Math.max(latestByChannel.get(s.channel) ?? 0, s.mtimeMs));
187
+ const key = JSON.stringify([s.agentId, s.channel]);
188
+ const prev = groups.get(key);
189
+ if (prev) {
190
+ prev.latest = Math.max(prev.latest, s.mtimeMs);
191
+ prev.count += 1;
192
+ } else {
193
+ groups.set(key, { agentId: s.agentId, channel: s.channel, latest: s.mtimeMs, count: 1 });
194
+ }
126
195
  }
127
196
  const out: ProjectFolder[] = [];
128
- for (const [channel, latest] of latestByChannel) {
197
+ for (const { agentId, channel, latest, count } of groups.values()) {
129
198
  const lastModified = new Date(latest);
130
199
  out.push({
131
- name: `openclaw-${channel}`,
132
- path: `openclaw:${channel}`,
200
+ name: openClawProjectName(agentId, channel),
201
+ path: openClawProjectPath(agentId, channel),
133
202
  isDirectory: true,
134
203
  lastModified,
135
204
  lastModifiedFormatted: formatDate(lastModified),
136
205
  cli: ["openclaw"],
206
+ sessionCount: count,
137
207
  });
138
208
  }
139
209
  out.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
@@ -146,18 +216,41 @@ export interface OpenClawProjectByName {
146
216
  }
147
217
 
148
218
  /** Resolve the OpenClaw sessions for a synthetic project name
149
- * (`openclaw-<channel>`), for the project-detail page. Session names use the
150
- * human-readable `origin.label` (e.g. "Chetan (@chhhee10) id:…") rather than
151
- * the raw session key. */
219
+ * (`openclaw-<agentId>-<channel>`), for the project-detail page. Session names
220
+ * use the human-readable `origin.label` (e.g. "Chetan (@chhhee10) id:…")
221
+ * rather than the raw session key. */
152
222
  export async function getOpenClawSessionsByEncodedName(
153
223
  name: string,
154
224
  ): Promise<OpenClawProjectByName> {
155
- if (!name.startsWith("openclaw-")) return { cwd: null, sessions: [] };
156
- const channel = name.slice("openclaw-".length);
225
+ const candidates = openClawProjectNameCandidates(name);
226
+ if (candidates.length === 0) return { cwd: null, sessions: [] };
227
+
157
228
  const sessions = await getOpenClawSessions();
158
- const matched = sessions.filter((s) => s.channel === channel);
229
+ const matching = (split: OpenClawNameSplit) =>
230
+ sessions.filter(
231
+ (s) => s.channel === split.channel && (split.agentId === null || s.agentId === split.agentId),
232
+ );
233
+
234
+ // Take the first split that actually owns sessions — length alone picks the
235
+ // wrong agent when one id is a hyphen-prefix of another. Falling back to the
236
+ // best guess keeps the page's labelling sensible when nothing matches.
237
+ let chosen = candidates[0];
238
+ let matched = matching(chosen);
239
+ if (matched.length === 0) {
240
+ for (const candidate of candidates.slice(1)) {
241
+ const hits = matching(candidate);
242
+ if (hits.length > 0) {
243
+ chosen = candidate;
244
+ matched = hits;
245
+ break;
246
+ }
247
+ }
248
+ }
249
+ const { agentId, channel } = chosen;
159
250
  return {
160
- cwd: `openclaw:${channel}`,
251
+ // Legacy channel-only names keep their old cwd label, since they really do
252
+ // span every agent on that channel.
253
+ cwd: agentId === null ? `openclaw:${channel}` : openClawProjectPath(agentId, channel),
161
254
  sessions: matched.map((s) => {
162
255
  const lastModified = new Date(s.mtimeMs);
163
256
  return {
@@ -195,6 +195,26 @@ export interface OpenClawTranscriptFile {
195
195
  sizeBytes: number;
196
196
  }
197
197
 
198
+ /**
199
+ * Agent ids configured under this OpenClaw home, in name order.
200
+ *
201
+ * An agentId is just a directory name (`main`, `weather-bot`) — OpenClaw
202
+ * validates nothing, so neither do we. Cheap on purpose: one readdir, no
203
+ * statting of transcripts, because the project-name parser calls this to
204
+ * longest-match an agent id out of a URL slug (agent ids may contain `-`, so
205
+ * splitting the slug is not an option).
206
+ */
207
+ export function listOpenClawAgents(): string[] {
208
+ try {
209
+ return readdirSync(join(openclawHome(), "agents"), { withFileTypes: true })
210
+ .filter((d) => d.isDirectory() && !d.name.startsWith("."))
211
+ .map((d) => d.name)
212
+ .sort();
213
+ } catch {
214
+ return [];
215
+ }
216
+ }
217
+
198
218
  /** Enumerate `agents/<agentId>/sessions/<uuid>.jsonl` transcripts, skipping the
199
219
  * heavy `.trajectory.jsonl` OTel traces and pointer files. */
200
220
  export function listOpenClawTranscripts(): OpenClawTranscriptFile[] {
@@ -29,6 +29,42 @@ export function decodeFolderName(name: string): string {
29
29
  return name.replace(/-/g, "/");
30
30
  }
31
31
 
32
+ /**
33
+ * Synthetic project path for gateway CLIs with no working directory —
34
+ * `hermes:<profile>:<source>`, `openclaw:<channel>`. Two or more leading letters
35
+ * so a Windows drive letter (`c:/code`) can never match.
36
+ */
37
+ const SYNTHETIC_PROJECT_PATH = /^[a-z]{2,}:/;
38
+
39
+ /**
40
+ * Whether a project `path` is a synthetic gateway path rather than a real
41
+ * filesystem path — `hermes:<profile>:<source>`, `openclaw:<agent>:<channel>`.
42
+ *
43
+ * These are the only projects with a meaningful segment hierarchy, so this is
44
+ * also what decides which rows the projects panel renders as a folder tree.
45
+ */
46
+ export function isSyntheticProjectPath(path?: string): boolean {
47
+ return !!path && SYNTHETIC_PROJECT_PATH.test(path);
48
+ }
49
+
50
+ /** Segments of a synthetic gateway path, outermost first. `[]` for real paths. */
51
+ export function syntheticPathSegments(path?: string): string[] {
52
+ return isSyntheticProjectPath(path) ? path!.split(":") : [];
53
+ }
54
+
55
+ /**
56
+ * Label to display for a project card.
57
+ *
58
+ * Filesystem-backed projects decode their folder name. Gateway projects must
59
+ * NOT: `decodeFolderName` turns every `-` into `/`, which splits a Hermes
60
+ * profile named `my-bot` into `my/bot`. Their `path` already carries the
61
+ * segments explicitly, so use it.
62
+ */
63
+ export function projectDisplayName(name: string, path?: string): string {
64
+ if (isSyntheticProjectPath(path)) return path!.replace(/:/g, "/");
65
+ return decodeFolderName(name);
66
+ }
67
+
32
68
  /**
33
69
  * Encodes a filesystem path into a Claude-compatible project folder name.
34
70
  * Inverse of `decodeFolderName`.