failproofai 1.0.3 → 1.0.4-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.next/standalone/.grok/hooks/failproofai.json +172 -0
  2. package/.next/standalone/.next/BUILD_ID +1 -1
  3. package/.next/standalone/.next/build-manifest.json +5 -5
  4. package/.next/standalone/.next/prerender-manifest.json +4 -4
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
  7. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  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 +6 -6
  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/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  23. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
  35. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  36. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  38. package/.next/standalone/.next/server/app/index.html +1 -1
  39. package/.next/standalone/.next/server/app/index.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  41. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  42. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  43. package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  49. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  50. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
  55. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
  64. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  65. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  66. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  68. package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
  69. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  70. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__0_gzk8q._.js} +1 -1
  73. package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1zndo1i._.js} +1 -1
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__07hor7h._.js +5 -0
  75. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0elsuli._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gnj_qa._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljuzm_._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w23bpg._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0x5c7sh._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yik9_w._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1jg6s9m._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1s6qci9._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xfm05z._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/_0tdy_6l._.js +3 -0
  90. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  92. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +21 -14
  93. package/.next/standalone/.next/server/chunks/lib_grok-projects_ts_1i2epl_._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/lib_qwen-projects_ts_0wu_a8e._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
  96. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
  97. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_4gcz8._.js +3 -0
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d--wmy._.js +3 -0
  104. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
  105. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
  108. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qqrlf_._.js +3 -0
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s11fx1._.js +3 -0
  110. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
  111. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0wvusu0._.js} +2 -2
  112. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1b5jzxr._.js → [root-of-the-server]__16gamwq._.js} +2 -2
  113. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bbk-si._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1c3ybxr._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1izi7zn._.js +3 -0
  116. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
  117. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zl_r0t._.js +1 -1
  119. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__21bj7uw._.js +3 -0
  120. package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/{_0v8eebd._.js → _00z5uuq._.js} +1 -1
  122. package/.next/standalone/.next/server/chunks/ssr/{_1if9ixi._.js → _08f0jd2._.js} +1 -1
  123. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/_0wso5d3._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/{_0j6gd_k._.js → _1byck5v._.js} +2 -2
  126. package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +155 -1
  127. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  128. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  130. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  131. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  133. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/ssr/lib_cline-projects_ts_13_hbdd._.js +3 -0
  135. package/.next/standalone/.next/server/chunks/ssr/lib_grok-projects_ts_0rc-d0o._.js +3 -0
  136. package/.next/standalone/.next/server/chunks/ssr/lib_ori-projects_ts_04tofqo._.js +3 -0
  137. package/.next/standalone/.next/server/chunks/ssr/lib_qwen-projects_ts_0w3i9o7._.js +3 -0
  138. package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
  139. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  140. package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
  141. package/.next/standalone/.next/server/pages/404.html +1 -1
  142. package/.next/standalone/.next/server/pages/500.html +1 -1
  143. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  144. package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
  145. package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 00gv0v91mycm-.js} +1 -1
  146. package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 01hvwwrhvbzyh.js} +1 -1
  147. package/.next/standalone/.next/static/chunks/0hzmxwb2pvk5p.js +6 -0
  148. package/.next/standalone/.next/static/chunks/{0nm11ysn0153h.js → 0j884dwnwtyrt.js} +1 -1
  149. package/.next/standalone/.next/static/chunks/0mr9dg3bnloa1.js +1 -0
  150. package/.next/standalone/.next/static/chunks/0qn9khv0oo9ex.js +1 -0
  151. package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 1_2jsatfuthrw.js} +1 -1
  152. package/.next/standalone/.next/static/chunks/1_bxotmf9qngs.js +1 -0
  153. package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
  154. package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 1po3lirgk2riw.js} +1 -1
  155. package/.next/standalone/.next/static/chunks/{40lpt2o2b283e.js → 2bnf_4pgyk1rq.js} +1 -1
  156. package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 2bvwsyu6x__ia.js} +1 -1
  157. package/.next/standalone/.next/static/chunks/{3l36d73kj18zw.js → 3_--3ibia6ba1.js} +1 -1
  158. package/.next/standalone/.next/static/chunks/3gf1jg-5m-u1t.css +2 -0
  159. package/.next/standalone/.next/static/chunks/{3pibxnbhjza7n.js → 3ws7eljkq25wg.js} +1 -1
  160. package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-18qf4jodtft6b.js} +1 -1
  161. package/.next/standalone/.qwen/settings.json +251 -0
  162. package/.next/standalone/app/project/[name]/page.tsx +24 -4
  163. package/.next/standalone/assets/logos/cline.png +0 -0
  164. package/.next/standalone/assets/logos/grok-dark.svg +1 -0
  165. package/.next/standalone/assets/logos/grok-light.svg +1 -0
  166. package/.next/standalone/assets/logos/ori-dark.svg +1 -0
  167. package/.next/standalone/assets/logos/ori-light.svg +1 -0
  168. package/.next/standalone/assets/logos/qwen.svg +1 -0
  169. package/.next/standalone/lib/cli-registry.ts +33 -1
  170. package/.next/standalone/lib/cline-projects.ts +155 -0
  171. package/.next/standalone/lib/cline-sessions.ts +268 -0
  172. package/.next/standalone/lib/download-session.ts +37 -0
  173. package/.next/standalone/lib/grok-projects.ts +123 -0
  174. package/.next/standalone/lib/grok-sessions.ts +355 -0
  175. package/.next/standalone/lib/ori-projects.ts +141 -0
  176. package/.next/standalone/lib/ori-sessions.ts +221 -0
  177. package/.next/standalone/lib/projects.ts +27 -3
  178. package/.next/standalone/lib/qwen-projects.ts +106 -0
  179. package/.next/standalone/lib/qwen-sessions.ts +313 -0
  180. package/.next/standalone/node_modules/@next/env/package.json +1 -1
  181. package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
  182. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
  183. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
  184. package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
  185. package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
  186. package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
  187. package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
  188. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
  189. package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
  190. package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +2 -2
  191. package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
  192. package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
  193. package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
  194. package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
  195. package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
  196. package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
  197. package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
  198. package/.next/standalone/node_modules/next/package.json +10 -10
  199. package/.next/standalone/package.json +11 -11
  200. package/.next/standalone/sdk/python/uv.lock +29 -29
  201. package/.next/standalone/server.js +1 -1
  202. package/README.md +38 -7
  203. package/bin/failproofai.mjs +24 -7
  204. package/dist/cli.mjs +20313 -17756
  205. package/dist/worker.mjs +8521 -6768
  206. package/lib/cli-registry.ts +33 -1
  207. package/lib/cline-projects.ts +155 -0
  208. package/lib/cline-sessions.ts +268 -0
  209. package/lib/download-session.ts +37 -0
  210. package/lib/grok-projects.ts +123 -0
  211. package/lib/grok-sessions.ts +355 -0
  212. package/lib/ori-projects.ts +141 -0
  213. package/lib/ori-sessions.ts +221 -0
  214. package/lib/projects.ts +27 -3
  215. package/lib/qwen-projects.ts +106 -0
  216. package/lib/qwen-sessions.ts +313 -0
  217. package/package.json +11 -11
  218. package/src/audit/cli-adapters/cline.ts +50 -0
  219. package/src/audit/cli-adapters/grok.ts +70 -0
  220. package/src/audit/cli-adapters/index.ts +24 -0
  221. package/src/audit/cli-adapters/ori.ts +55 -0
  222. package/src/audit/cli-adapters/qwen.ts +71 -0
  223. package/src/hooks/batch-expand.ts +380 -0
  224. package/src/hooks/batch-fanout.ts +142 -0
  225. package/src/hooks/builtin-policies.ts +6 -4
  226. package/src/hooks/enforcement-capability.ts +97 -0
  227. package/src/hooks/handler.ts +61 -5
  228. package/src/hooks/harness-cli.ts +3 -1
  229. package/src/hooks/integrations.ts +756 -2
  230. package/src/hooks/normalize-cli-payload.ts +126 -0
  231. package/src/hooks/policy-evaluator.ts +297 -6
  232. package/src/hooks/resolve-transcript-path.ts +10 -0
  233. package/src/hooks/risk-patterns.ts +26 -0
  234. package/src/hooks/tool-name-canonicalize.ts +64 -0
  235. package/src/hooks/types.ts +533 -2
  236. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +0 -3
  237. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +0 -5
  238. package/.next/standalone/.next/server/chunks/_1-akg6w._.js +0 -3
  239. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00-s7h8._.js +0 -3
  240. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +0 -3
  241. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
  242. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +0 -3
  243. package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
  244. package/.next/standalone/.next/static/chunks/1-_zkmnm-jwei.js +0 -1
  245. package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
  246. package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
  247. package/.next/standalone/.next/static/chunks/40nr6ehj4p3cw.css +0 -2
  248. package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
  249. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_buildManifest.js +0 -0
  250. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_clientMiddlewareManifest.js +0 -0
  251. /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_ssgManifest.js +0 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Ori (OpenRouter's ori) session transcript loader + parser.
3
+ *
4
+ * AUDIT-ONLY (Pillar 2). Where ori keeps a transcript is not obvious, and three
5
+ * plausible places are dead ends worth naming so nobody re-derives them:
6
+ *
7
+ * • `<cwd>/.ori/logs/sessions/<uuid>/metadata.json` holds ONLY a summary —
8
+ * harness, `runIds[{cwd, model, prompt, runId}]`, token `usage`, turn
9
+ * counts, timestamps. Verified on a successful tool-using session: no
10
+ * messages, no tool calls.
11
+ * • `<cwd>/.ori/logs/code-<ISO>.jsonl` is runtime LIFECYCLE logging —
12
+ * formatted log lines, zero tool records (grepped a full session: 0 hits).
13
+ * • The rich `AgentRuntimeEvent` stream exists only transiently on
14
+ * `ori code --output jsonl` stdout; nothing persists it.
15
+ *
16
+ * The real transcript is SQLite, in ori's GLOBAL workspace at
17
+ * `~/.ori/global/.ori/state.sqlite` (bare `ori` boots the global workspace, not
18
+ * the project's, so one DB holds every project's sessions):
19
+ *
20
+ * ori_agent_loop_sessions(session_id TEXT PK, title, cwd, model, turns,
21
+ * cost_usd REAL, cost_known INT, archived INT,
22
+ * first_prompt, parent_session_id, updated_at TEXT ISO)
23
+ * ori_agent_loop_history(session_id TEXT PK, prompt TEXT = the whole
24
+ * serialized conversation as JSON, revision, updated_at)
25
+ *
26
+ * `ori_agent_loop_history.prompt` is misnamed — it is not the prompt, it is the
27
+ * full conversation: `{"content":[{role, content}, …]}` with Claude-style typed
28
+ * blocks on the assistant side (`{type:"reasoning"|"text"}`,
29
+ * `{type:"tool-call", id, name, params}`) and results in `role:"tool"` messages
30
+ * (`{type:"tool-result", id, name, isFailure, result:[{type:"input_text",text}]}`).
31
+ * `oriMessagesToLogEntries` pairs each tool-call with its tool-result by id and
32
+ * is a PURE function of the parsed JSON, so it is unit-testable without a DB.
33
+ *
34
+ * LIMITATION, and it is inherent rather than a shortcut: ori stores NO
35
+ * per-message timestamp. Every entry in a session therefore carries the
36
+ * session's own `updated_at`, so relative ordering within a session is the
37
+ * array order and per-message timing is not recoverable.
38
+ *
39
+ * Home override: `ORI_HOME` (the dir containing `global/`), or `ORI_DB_PATH` to
40
+ * point straight at a state.sqlite.
41
+ */
42
+ import { homedir } from "node:os";
43
+ import { join } from "node:path";
44
+ import { openSqliteReadonly } from "./sqlite-reader";
45
+ import { runtimeCache } from "./runtime-cache";
46
+ import {
47
+ baseEntry,
48
+ formatTimestamp,
49
+ type LogEntry,
50
+ type UserEntry,
51
+ type AssistantEntry,
52
+ type ContentBlock,
53
+ type ToolUseBlock,
54
+ type LogSource,
55
+ } from "./log-entries";
56
+ import { formatDuration } from "./format-duration";
57
+
58
+ /** Absolute path to ori's home (override with ORI_HOME). */
59
+ export function oriHome(): string {
60
+ return process.env.ORI_HOME || join(homedir(), ".ori");
61
+ }
62
+
63
+ /** Absolute path to ori's state DB (override with ORI_DB_PATH). */
64
+ export function oriDbPath(): string {
65
+ return process.env.ORI_DB_PATH || join(oriHome(), "global", ".ori", "state.sqlite");
66
+ }
67
+
68
+ /** ori writes ISO-8601 strings; return epoch ms, 0 when absent or unparseable. */
69
+ export function oriTimestampToMs(value: string | null | undefined): number {
70
+ if (!value) return 0;
71
+ const ms = Date.parse(value);
72
+ return Number.isNaN(ms) ? 0 : ms;
73
+ }
74
+
75
+ interface OriBlock {
76
+ type?: string;
77
+ text?: string;
78
+ id?: string;
79
+ name?: string;
80
+ params?: Record<string, unknown>;
81
+ isFailure?: boolean;
82
+ result?: Array<{ type?: string; text?: string }>;
83
+ }
84
+
85
+ interface OriMessage {
86
+ role?: string;
87
+ content?: string | OriBlock[];
88
+ }
89
+
90
+ /** Flatten a tool-result's `result` array into display text. */
91
+ function toolResultText(block: OriBlock): string {
92
+ if (!Array.isArray(block.result)) return "";
93
+ return block.result
94
+ .map((r) => (typeof r.text === "string" ? r.text : ""))
95
+ .filter((t) => t.length > 0)
96
+ .join("\n");
97
+ }
98
+
99
+ /**
100
+ * Parse ori's serialized conversation into the shared LogEntry[] shape.
101
+ *
102
+ * PURE — takes already-parsed JSON so tests need no SQLite. `system` messages
103
+ * are dropped: ori's is a static multi-kilobyte agent prompt, identical in
104
+ * every session, and carrying it would swamp the audit with boilerplate.
105
+ */
106
+ export function oriMessagesToLogEntries(
107
+ messages: OriMessage[],
108
+ sessionId: string,
109
+ timestampMs: number,
110
+ source: LogSource = "session",
111
+ ): LogEntry[] {
112
+ const date = new Date(timestampMs);
113
+ const timestamp = date.toISOString();
114
+ const entries: LogEntry[] = [];
115
+ // tool-call id → the block awaiting its result, so a later `role:"tool"`
116
+ // message can attach onto the assistant turn that made the call.
117
+ const pending = new Map<string, ToolUseBlock>();
118
+
119
+ messages.forEach((msg, i) => {
120
+ const role = msg.role;
121
+ if (role === "system") return;
122
+
123
+ const raw = { uuid: `${sessionId}:${i}`, parentUuid: null } as Record<string, unknown>;
124
+ const base = baseEntry(raw, timestamp, date, source);
125
+
126
+ if (role === "user") {
127
+ const text = typeof msg.content === "string"
128
+ ? msg.content
129
+ : (msg.content ?? []).map((b) => b.text ?? "").join("");
130
+ entries.push({ ...base, type: "user", message: { role: "user", content: text } } as UserEntry);
131
+ return;
132
+ }
133
+
134
+ if (role === "assistant") {
135
+ const blocks: ContentBlock[] = [];
136
+ for (const b of Array.isArray(msg.content) ? msg.content : []) {
137
+ if (b.type === "reasoning" && typeof b.text === "string") {
138
+ blocks.push({ type: "thinking", thinking: b.text });
139
+ } else if (b.type === "text" && typeof b.text === "string") {
140
+ blocks.push({ type: "text", text: b.text });
141
+ } else if (b.type === "tool-call" && typeof b.id === "string") {
142
+ const block: ToolUseBlock = {
143
+ type: "tool_use",
144
+ id: b.id,
145
+ name: b.name ?? "",
146
+ input: b.params ?? {},
147
+ };
148
+ pending.set(b.id, block);
149
+ blocks.push(block);
150
+ }
151
+ }
152
+ if (blocks.length > 0) {
153
+ entries.push({ ...base, type: "assistant", message: { role: "assistant", content: blocks } } as AssistantEntry);
154
+ }
155
+ return;
156
+ }
157
+
158
+ if (role === "tool") {
159
+ for (const b of Array.isArray(msg.content) ? msg.content : []) {
160
+ if (b.type !== "tool-result" || typeof b.id !== "string") continue;
161
+ const target = pending.get(b.id);
162
+ if (!target) continue;
163
+ // No per-message timestamp exists, so duration is unknowable — 0 rather
164
+ // than a fabricated span.
165
+ target.result = {
166
+ timestamp,
167
+ timestampFormatted: formatTimestamp(date),
168
+ content: toolResultText(b),
169
+ durationMs: 0,
170
+ durationFormatted: formatDuration(0),
171
+ };
172
+ pending.delete(b.id);
173
+ }
174
+ return;
175
+ }
176
+ });
177
+
178
+ return entries;
179
+ }
180
+
181
+ export interface OriSessionLog {
182
+ entries: LogEntry[];
183
+ cwd?: string;
184
+ }
185
+
186
+ /** Load and parse one ori session. Returns null when absent or unreadable. */
187
+ export async function getOriSessionLog(sessionId: string): Promise<OriSessionLog | null> {
188
+ const db = await openSqliteReadonly(oriDbPath());
189
+ if (!db) return null;
190
+ try {
191
+ const rows = db.query<{ prompt: string | null; updated_at: string | null; cwd: string | null }>(
192
+ "SELECT h.prompt AS prompt, h.updated_at AS updated_at, s.cwd AS cwd " +
193
+ "FROM ori_agent_loop_history h " +
194
+ "LEFT JOIN ori_agent_loop_sessions s ON s.session_id = h.session_id " +
195
+ "WHERE h.session_id = ?",
196
+ [sessionId],
197
+ );
198
+ const row = rows[0];
199
+ if (!row || !row.prompt) return null;
200
+ let parsed: { content?: OriMessage[] };
201
+ try {
202
+ parsed = JSON.parse(row.prompt) as { content?: OriMessage[] };
203
+ } catch {
204
+ return null;
205
+ }
206
+ return {
207
+ entries: oriMessagesToLogEntries(
208
+ parsed.content ?? [],
209
+ sessionId,
210
+ oriTimestampToMs(row.updated_at),
211
+ ),
212
+ cwd: row.cwd ?? undefined,
213
+ };
214
+ } catch {
215
+ return null;
216
+ } finally {
217
+ db.close();
218
+ }
219
+ }
220
+
221
+ export const getCachedOriSessionLog = runtimeCache(getOriSessionLog, 2);
package/lib/projects.ts CHANGED
@@ -16,7 +16,7 @@ import { formatDate } from "./format-date";
16
16
  export const UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
17
17
  export const PATH_TRAVERSAL_RE = /(^|[\\/])\.\.($|[\\/])/;
18
18
 
19
- export type ProjectCli = "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "hermes" | "openclaw" | "factory" | "devin" | "antigravity" | "goose";
19
+ export type ProjectCli = "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "hermes" | "openclaw" | "factory" | "devin" | "antigravity" | "goose" | "grok" | "qwen" | "ori" | "cline";
20
20
 
21
21
  export interface ProjectFolder {
22
22
  name: string;
@@ -171,6 +171,10 @@ export async function getProjectFolders(): Promise<ProjectFolder[]> {
171
171
  { getDevinProjects },
172
172
  { getAntigravityProjects },
173
173
  { getGooseProjects },
174
+ { getGrokProjects },
175
+ { getQwenProjects },
176
+ { getOriProjects },
177
+ { getClineProjects },
174
178
  ] = await Promise.all([
175
179
  import("./codex-projects"),
176
180
  import("./copilot-projects"),
@@ -183,8 +187,12 @@ export async function getProjectFolders(): Promise<ProjectFolder[]> {
183
187
  import("./devin-projects"),
184
188
  import("./antigravity-projects"),
185
189
  import("./goose-projects"),
190
+ import("./grok-projects"),
191
+ import("./qwen-projects"),
192
+ import("./ori-projects"),
193
+ import("./cline-projects"),
186
194
  ]);
187
- const [claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose] = await Promise.all([
195
+ const [claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose, grok, qwen, ori, cline] = await Promise.all([
188
196
  getClaudeProjectFolders(),
189
197
  getCodexProjects().catch((error) => {
190
198
  logError("Error reading Codex projects:", error);
@@ -230,8 +238,24 @@ export async function getProjectFolders(): Promise<ProjectFolder[]> {
230
238
  logError("Error reading Goose projects:", error);
231
239
  return [] as ProjectFolder[];
232
240
  }),
241
+ getGrokProjects().catch((error) => {
242
+ logError("Error reading grok projects:", error);
243
+ return [] as ProjectFolder[];
244
+ }),
245
+ getQwenProjects().catch((error) => {
246
+ logError("Error reading Qwen projects:", error);
247
+ return [] as ProjectFolder[];
248
+ }),
249
+ getOriProjects().catch((error) => {
250
+ logError("Error reading Ori projects:", error);
251
+ return [] as ProjectFolder[];
252
+ }),
253
+ getClineProjects().catch((error) => {
254
+ logError("Error reading Cline projects:", error);
255
+ return [] as ProjectFolder[];
256
+ }),
233
257
  ]);
234
- return mergeProjectFolders(claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose);
258
+ return mergeProjectFolders(claude, codex, copilot, cursor, opencode, pi, hermes, openclaw, factory, devin, antigravity, goose, grok, qwen, ori, cline);
235
259
  }
236
260
 
237
261
  /**
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Qwen Code project discovery.
3
+ *
4
+ * qwen stores transcripts at
5
+ * `~/.qwen/projects/<encoded-cwd>/chats/<uuid>.jsonl`, using the same
6
+ * Claude-style encoded-cwd folder names (`-home-user-project`) as Claude Code.
7
+ * The encoded folder doubles as the URL slug for `/project/[name]`, so a cwd
8
+ * present in both stores naturally produces the same `name` and merges on the
9
+ * Claude side (see `mergeProjectFolders` in lib/projects.ts).
10
+ */
11
+ import { decodeFolderName } from "./paths";
12
+ import { listQwenTranscripts, getQwenSessionLog } from "./qwen-sessions";
13
+ import type { ProjectFolder, SessionFile } from "./projects";
14
+ import { runtimeCache } from "./runtime-cache";
15
+ import { formatDate } from "./format-date";
16
+ import { logWarn } from "./logger";
17
+
18
+ /** Returns one ProjectFolder per encoded-cwd folder discovered under
19
+ * ~/.qwen/projects/. */
20
+ export async function getQwenProjects(): Promise<ProjectFolder[]> {
21
+ let transcripts;
22
+ try {
23
+ transcripts = listQwenTranscripts();
24
+ } catch (error) {
25
+ logWarn("Failed to scan Qwen sessions:", error);
26
+ return [];
27
+ }
28
+
29
+ const byName = new Map<string, { latest: number; cwd: string; name: string }>();
30
+ for (const t of transcripts) {
31
+ const existing = byName.get(t.projectName);
32
+ if (!existing || t.mtimeMs > existing.latest) {
33
+ byName.set(t.projectName, { latest: t.mtimeMs, cwd: t.cwd, name: t.projectName });
34
+ }
35
+ }
36
+
37
+ const folders: ProjectFolder[] = [];
38
+ for (const { name, cwd, latest } of byName.values()) {
39
+ const lastModified = new Date(latest);
40
+ folders.push({
41
+ name,
42
+ path: cwd,
43
+ isDirectory: true,
44
+ lastModified,
45
+ lastModifiedFormatted: formatDate(lastModified),
46
+ cli: ["qwen"],
47
+ });
48
+ }
49
+ folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
50
+ return folders;
51
+ }
52
+
53
+ export interface QwenProjectByName {
54
+ /** Canonical cwd recovered from a session line (the folder decode is lossy).
55
+ * Null when no session could be read. */
56
+ cwd: string | null;
57
+ sessions: SessionFile[];
58
+ }
59
+
60
+ /**
61
+ * Look up Qwen sessions for a project URL slug (the encoded-cwd folder name).
62
+ * qwen names its folders with the same encoding Claude uses, so the slug
63
+ * matches the folder directly. The canonical cwd is recovered from the newest
64
+ * session's own `cwd` field (folder decode is lossy for cwds containing `-`).
65
+ */
66
+ export async function getQwenSessionsByEncodedName(name: string): Promise<QwenProjectByName> {
67
+ let transcripts;
68
+ try {
69
+ transcripts = listQwenTranscripts().filter((t) => t.projectName === name);
70
+ } catch (error) {
71
+ logWarn("Failed to scan Qwen sessions:", error);
72
+ return { cwd: null, sessions: [] };
73
+ }
74
+ if (transcripts.length === 0) return { cwd: null, sessions: [] };
75
+
76
+ const sorted = [...transcripts].sort((a, b) => b.mtimeMs - a.mtimeMs);
77
+
78
+ let cwd: string | null = null;
79
+ try {
80
+ const log = await getQwenSessionLog(sorted[0].sessionId);
81
+ cwd = log?.cwd ?? null;
82
+ } catch {
83
+ // best-effort — fall back to the lossy decode below
84
+ }
85
+ if (!cwd) cwd = decodeFolderName(name);
86
+
87
+ const sessions: SessionFile[] = sorted.map((t) => {
88
+ const lastModified = new Date(t.mtimeMs);
89
+ return {
90
+ name: t.sessionId,
91
+ path: t.transcriptPath,
92
+ lastModified,
93
+ lastModifiedFormatted: formatDate(lastModified),
94
+ sessionId: t.sessionId,
95
+ cli: "qwen" as const,
96
+ };
97
+ });
98
+ return { cwd, sessions };
99
+ }
100
+
101
+ export const getCachedQwenProjects = runtimeCache(getQwenProjects, 30);
102
+ export const getCachedQwenSessionsByEncodedName = runtimeCache(
103
+ (name: string) => getQwenSessionsByEncodedName(name),
104
+ 30,
105
+ { maxSize: 50 },
106
+ );
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Qwen Code session transcript loader + parser.
3
+ *
4
+ * AUDIT-ONLY (Pillar 2). qwen writes one JSONL transcript per session at
5
+ * `~/.qwen/projects/<encoded-cwd>/chats/<sessionId>.jsonl` — Claude-style
6
+ * encoded-cwd folders (e.g. `-home-chetan-project`), but note the extra
7
+ * `chats/` level, which Claude/Factory do not have. Verified live against
8
+ * @qwen-code/qwen-code 0.21.12.
9
+ *
10
+ * Every line carries `{uuid, parentUuid, sessionId, cwd, timestamp, version,
11
+ * type}`. `type` is one of:
12
+ * "user" — message.parts, role "user"
13
+ * "assistant" — message.parts, role "model" (Gemini's name for assistant)
14
+ * "tool_result" — message.parts + a `toolCallResult` sidecar
15
+ * "system" — bookkeeping (subtype/systemPayload); skipped
16
+ *
17
+ * The message body is **Gemini-shaped, not Claude-shaped**: `message.parts[]`
18
+ * holds `{text}`, `{functionCall:{id,name,args}}`, and
19
+ * `{functionResponse:{id,name,response}}` — so this is NOT a clone of
20
+ * lib/factory-sessions.ts despite the similar on-disk layout. `functionCall` is
21
+ * paired with its later `functionResponse` by id (mirroring the tool_use /
22
+ * tool_result pairing elsewhere). `qwenLinesToLogEntries` is PURE, so it is
23
+ * unit-testable with plain line objects.
24
+ *
25
+ * Home override: set `QWEN_HOME` (used by tests / to point at a copied
26
+ * projects dir).
27
+ */
28
+ import { readFile } from "node:fs/promises";
29
+ import { readdirSync, statSync } from "node:fs";
30
+ import { homedir } from "node:os";
31
+ import { join } from "node:path";
32
+ import { decodeFolderName } from "./paths";
33
+ import { runtimeCache } from "./runtime-cache";
34
+ import {
35
+ baseEntry,
36
+ formatTimestamp,
37
+ parseRawLines,
38
+ type LogEntry,
39
+ type UserEntry,
40
+ type AssistantEntry,
41
+ type GenericEntry,
42
+ type ContentBlock,
43
+ type ToolUseBlock,
44
+ type LogSource,
45
+ } from "./log-entries";
46
+ import { formatDuration } from "./format-duration";
47
+
48
+ /** Qwen sessions are stored under UUID filenames. */
49
+ export const QWEN_SESSION_ID_RE =
50
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
51
+
52
+ /** Absolute path to Qwen's config home (override with QWEN_HOME). */
53
+ export function qwenHome(): string {
54
+ return process.env.QWEN_HOME || join(homedir(), ".qwen");
55
+ }
56
+
57
+ /** Absolute path to the Qwen projects root. */
58
+ export function qwenProjectsRoot(): string {
59
+ return join(qwenHome(), "projects");
60
+ }
61
+
62
+ // ── Parsing helpers ──
63
+
64
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
65
+ return !!v && typeof v === "object" && !Array.isArray(v);
66
+ }
67
+
68
+ /** Flatten a Gemini `response` payload (or any tool output) to display text. */
69
+ function extractResponseText(response: unknown): string {
70
+ if (typeof response === "string") return response;
71
+ if (!isPlainObject(response)) return "";
72
+ for (const key of ["output", "result", "content", "error"]) {
73
+ const v = response[key];
74
+ if (typeof v === "string" && v.length > 0) return v;
75
+ }
76
+ return JSON.stringify(response);
77
+ }
78
+
79
+ function toDate(value: unknown, fallbackMs: number): Date {
80
+ if (typeof value === "number" && Number.isFinite(value)) {
81
+ if (value > 1e12) return new Date(value);
82
+ if (value > 1e9) return new Date(value * 1000);
83
+ }
84
+ if (typeof value === "string") {
85
+ const ms = Date.parse(value);
86
+ if (!Number.isNaN(ms)) return new Date(ms);
87
+ }
88
+ return new Date(fallbackMs);
89
+ }
90
+
91
+ // ── Pure parser: transcript lines → LogEntry[] ──
92
+
93
+ /**
94
+ * Convert Qwen transcript JSONL lines (parsed objects, in file order) into
95
+ * `LogEntry[]`. `system` lines are skipped. Assistant `functionCall` parts are
96
+ * paired with their later `functionResponse` by call id. Pure — unit-testable
97
+ * with plain line objects.
98
+ */
99
+ export function qwenLinesToLogEntries(
100
+ lines: Record<string, unknown>[],
101
+ source: LogSource = "session",
102
+ ): LogEntry[] {
103
+ const entries: LogEntry[] = [];
104
+ const toolUseById = new Map<string, ToolUseBlock>();
105
+ const toolUseStartMs = new Map<string, number>();
106
+ const baseMs = Date.now();
107
+
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const line = lines[i];
110
+ if (!isPlainObject(line)) continue;
111
+ const type = typeof line.type === "string" ? line.type : "";
112
+ if (type === "system") continue; // subtype/systemPayload bookkeeping
113
+
114
+ const m = isPlainObject(line.message) ? line.message : undefined;
115
+ if (!m) continue;
116
+ const parts = Array.isArray(m.parts) ? m.parts : [];
117
+
118
+ const date = toDate(line.timestamp, baseMs + i);
119
+ const timestamp = date.toISOString();
120
+ const raw: Record<string, unknown> = {
121
+ uuid: line.uuid != null ? String(line.uuid) : `qwen-${i}`,
122
+ parentUuid: line.parentUuid != null ? String(line.parentUuid) : null,
123
+ };
124
+ const base = baseEntry(raw, timestamp, date, source);
125
+
126
+ // Assistant turn: text parts and/or functionCall parts. Qwen names the
127
+ // assistant role "model" (Gemini lineage); accept both spellings.
128
+ if (type === "assistant" || m.role === "model" || m.role === "assistant") {
129
+ const blocks: ContentBlock[] = [];
130
+ for (const p of parts) {
131
+ if (!isPlainObject(p)) continue;
132
+ if (typeof p.text === "string" && p.text.length > 0) {
133
+ blocks.push({ type: "text", text: p.text });
134
+ continue;
135
+ }
136
+ const fc = isPlainObject(p.functionCall) ? p.functionCall : undefined;
137
+ if (fc) {
138
+ const id = typeof fc.id === "string" ? fc.id : `${String(fc.name ?? "tool")}-${blocks.length}`;
139
+ const name = typeof fc.name === "string" ? fc.name : "tool";
140
+ const input = isPlainObject(fc.args) ? fc.args : {};
141
+ const block: ToolUseBlock = { type: "tool_use", id, name, input };
142
+ blocks.push(block);
143
+ toolUseById.set(id, block);
144
+ toolUseStartMs.set(id, date.getTime());
145
+ }
146
+ }
147
+ if (blocks.length === 0) continue; // empty / failed assistant turn
148
+ entries.push({
149
+ type: "assistant",
150
+ ...base,
151
+ message: {
152
+ role: "assistant",
153
+ content: blocks,
154
+ model: typeof line.model === "string" ? line.model : undefined,
155
+ },
156
+ } satisfies AssistantEntry);
157
+ continue;
158
+ }
159
+
160
+ // tool_result / user: functionResponse parts attach to their call; plain
161
+ // text parts become a user turn.
162
+ let attachedAny = false;
163
+ const textParts: string[] = [];
164
+ for (const p of parts) {
165
+ if (!isPlainObject(p)) continue;
166
+ const fr = isPlainObject(p.functionResponse) ? p.functionResponse : undefined;
167
+ if (fr) {
168
+ const callId = typeof fr.id === "string" ? fr.id : undefined;
169
+ const block = callId ? toolUseById.get(callId) : undefined;
170
+ if (block) {
171
+ const startMs = (callId && toolUseStartMs.get(callId)) || date.getTime();
172
+ const durationMs = Math.max(0, date.getTime() - startMs);
173
+ // Prefer the sidecar's rendered text — it is what the TUI showed —
174
+ // and fall back to flattening the raw functionResponse payload.
175
+ const sidecar = isPlainObject(line.toolCallResult) ? line.toolCallResult : undefined;
176
+ const display =
177
+ sidecar && typeof sidecar.resultDisplay === "string" && sidecar.resultDisplay.length > 0
178
+ ? sidecar.resultDisplay
179
+ : extractResponseText(fr.response);
180
+ block.result = {
181
+ timestamp,
182
+ timestampFormatted: formatTimestamp(date),
183
+ content: display,
184
+ durationMs,
185
+ durationFormatted: formatDuration(durationMs),
186
+ };
187
+ attachedAny = true;
188
+ continue;
189
+ }
190
+ }
191
+ if (typeof p.text === "string" && p.text.length > 0) textParts.push(p.text);
192
+ }
193
+ if (textParts.length > 0) {
194
+ entries.push({
195
+ type: "user",
196
+ ...base,
197
+ message: { role: "user", content: textParts.join("\n") },
198
+ } satisfies UserEntry);
199
+ } else if (!attachedAny) {
200
+ entries.push({ type: "system", ...base, raw } satisfies GenericEntry);
201
+ }
202
+ }
203
+
204
+ entries.sort((a, b) => a.timestampMs - b.timestampMs);
205
+ return entries;
206
+ }
207
+
208
+ // ── Discovery + file loader ──
209
+
210
+ export interface QwenTranscriptFile {
211
+ /** Encoded folder name on disk (e.g. "-home-user-project"). */
212
+ projectName: string;
213
+ /** Decoded cwd of the project (lossy; canonical cwd lives on every line). */
214
+ cwd: string;
215
+ sessionId: string;
216
+ transcriptPath: string;
217
+ mtimeMs: number;
218
+ sizeBytes: number;
219
+ }
220
+
221
+ /** Enumerate `projects/<encoded-cwd>/chats/<uuid>.jsonl` transcripts. */
222
+ export function listQwenTranscripts(): QwenTranscriptFile[] {
223
+ const root = qwenProjectsRoot();
224
+ const out: QwenTranscriptFile[] = [];
225
+ let projectDirs: import("node:fs").Dirent[];
226
+ try {
227
+ projectDirs = readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory());
228
+ } catch {
229
+ return out;
230
+ }
231
+ for (const dir of projectDirs) {
232
+ const projectName = dir.name;
233
+ const cwd = decodeFolderName(projectName);
234
+ // Transcripts live one level deeper than Claude/Factory, under `chats/`.
235
+ const chatsPath = join(root, projectName, "chats");
236
+ let files: string[];
237
+ try {
238
+ files = readdirSync(chatsPath);
239
+ } catch {
240
+ continue;
241
+ }
242
+ for (const file of files) {
243
+ if (!file.endsWith(".jsonl")) continue;
244
+ const sessionId = file.slice(0, -".jsonl".length);
245
+ if (!QWEN_SESSION_ID_RE.test(sessionId)) continue;
246
+ const transcriptPath = join(chatsPath, file);
247
+ try {
248
+ const st = statSync(transcriptPath);
249
+ out.push({
250
+ projectName,
251
+ cwd,
252
+ sessionId,
253
+ transcriptPath,
254
+ mtimeMs: st.mtimeMs,
255
+ sizeBytes: st.size,
256
+ });
257
+ } catch {
258
+ // skip unreadable
259
+ }
260
+ }
261
+ }
262
+ return out;
263
+ }
264
+
265
+ /** Resolve a session UUID to its on-disk transcript path (host-side). Guards
266
+ * against traversal by requiring a UUID filename. Shared by the audit adapter,
267
+ * the hook transcript resolver, and download-session. Synchronous so the hook
268
+ * hot path can call it without awaits. */
269
+ export function findQwenTranscript(sessionId: string): string | null {
270
+ if (!QWEN_SESSION_ID_RE.test(sessionId)) return null;
271
+ for (const t of listQwenTranscripts()) {
272
+ if (t.sessionId === sessionId) return t.transcriptPath;
273
+ }
274
+ return null;
275
+ }
276
+
277
+ export interface QwenSessionLogData {
278
+ entries: LogEntry[];
279
+ rawLines: Record<string, unknown>[];
280
+ cwd?: string;
281
+ filePath: string;
282
+ }
283
+
284
+ /** Load and parse one session transcript by UUID. Returns `null` when the file
285
+ * is missing/unreadable or the id fails validation. */
286
+ export async function getQwenSessionLog(sessionId: string): Promise<QwenSessionLogData | null> {
287
+ const filePath = findQwenTranscript(sessionId);
288
+ if (!filePath) return null;
289
+ let content: string;
290
+ try {
291
+ content = await readFile(filePath, "utf-8");
292
+ } catch {
293
+ return null;
294
+ }
295
+ const rawLines = parseRawLines(content, "session");
296
+ const entries = qwenLinesToLogEntries(rawLines, "session");
297
+ // Every line carries the real cwd; take the first one rather than trusting
298
+ // the lossy decoded folder name.
299
+ let cwd: string | undefined;
300
+ for (const line of rawLines) {
301
+ if (isPlainObject(line) && typeof line.cwd === "string" && line.cwd.length > 0) {
302
+ cwd = line.cwd;
303
+ break;
304
+ }
305
+ }
306
+ return { entries, rawLines, cwd, filePath };
307
+ }
308
+
309
+ export const getCachedQwenSessionLog = runtimeCache(
310
+ (sessionId: string) => getQwenSessionLog(sessionId),
311
+ 60,
312
+ { maxSize: 50 },
313
+ );