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.
- package/.next/standalone/.grok/hooks/failproofai.json +172 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0u3wrvq._.js → [externals]__0_gzk8q._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0-9ti7z._.js → [externals]__1zndo1i._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07hor7h._.js +5 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cuho4x._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0elsuli._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0gnj_qa._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljuzm_._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w23bpg._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0x5c7sh._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yik9_w._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1jg6s9m._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1s6qci9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xfm05z._.js +3 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tdy_6l._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +21 -14
- package/.next/standalone/.next/server/chunks/lib_grok-projects_ts_1i2epl_._.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_qwen-projects_ts_0wu_a8e._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_4gcz8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0d--wmy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qqrlf_._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s11fx1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0n_lxhg._.js → [root-of-the-server]__0wvusu0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1b5jzxr._.js → [root-of-the-server]__16gamwq._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bbk-si._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1c3ybxr._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1izi7zn._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qapotl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zl_r0t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__21bj7uw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0-yi74u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0v8eebd._.js → _00z5uuq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1if9ixi._.js → _08f0jd2._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0wso5d3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0j6gd_k._.js → _1byck5v._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1es2j7i._.js +155 -1
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_cline-projects_ts_13_hbdd._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_grok-projects_ts_0rc-d0o._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_ori-projects_ts_04tofqo._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_qwen-projects_ts_0w3i9o7._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +19 -19
- package/.next/standalone/.next/static/chunks/{0ollg-nnu3t3e.js → 00gv0v91mycm-.js} +1 -1
- package/.next/standalone/.next/static/chunks/{354-n7p7labpn.js → 01hvwwrhvbzyh.js} +1 -1
- package/.next/standalone/.next/static/chunks/0hzmxwb2pvk5p.js +6 -0
- package/.next/standalone/.next/static/chunks/{0nm11ysn0153h.js → 0j884dwnwtyrt.js} +1 -1
- package/.next/standalone/.next/static/chunks/0mr9dg3bnloa1.js +1 -0
- package/.next/standalone/.next/static/chunks/0qn9khv0oo9ex.js +1 -0
- package/.next/standalone/.next/static/chunks/{2xm5mj1d21b4n.js → 1_2jsatfuthrw.js} +1 -1
- package/.next/standalone/.next/static/chunks/1_bxotmf9qngs.js +1 -0
- package/.next/standalone/.next/static/chunks/1eyl7fzkmnkf_.js +1 -0
- package/.next/standalone/.next/static/chunks/{42yfkhekx440s.js → 1po3lirgk2riw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{40lpt2o2b283e.js → 2bnf_4pgyk1rq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1a6u7l3giejch.js → 2bvwsyu6x__ia.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3l36d73kj18zw.js → 3_--3ibia6ba1.js} +1 -1
- package/.next/standalone/.next/static/chunks/3gf1jg-5m-u1t.css +2 -0
- package/.next/standalone/.next/static/chunks/{3pibxnbhjza7n.js → 3ws7eljkq25wg.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-2x-o099q64i8h.js → turbopack-18qf4jodtft6b.js} +1 -1
- package/.next/standalone/.qwen/settings.json +251 -0
- package/.next/standalone/app/project/[name]/page.tsx +24 -4
- package/.next/standalone/assets/logos/cline.png +0 -0
- package/.next/standalone/assets/logos/grok-dark.svg +1 -0
- package/.next/standalone/assets/logos/grok-light.svg +1 -0
- package/.next/standalone/assets/logos/ori-dark.svg +1 -0
- package/.next/standalone/assets/logos/ori-light.svg +1 -0
- package/.next/standalone/assets/logos/qwen.svg +1 -0
- package/.next/standalone/lib/cli-registry.ts +33 -1
- package/.next/standalone/lib/cline-projects.ts +155 -0
- package/.next/standalone/lib/cline-sessions.ts +268 -0
- package/.next/standalone/lib/download-session.ts +37 -0
- package/.next/standalone/lib/grok-projects.ts +123 -0
- package/.next/standalone/lib/grok-sessions.ts +355 -0
- package/.next/standalone/lib/ori-projects.ts +141 -0
- package/.next/standalone/lib/ori-sessions.ts +221 -0
- package/.next/standalone/lib/projects.ts +27 -3
- package/.next/standalone/lib/qwen-projects.ts +106 -0
- package/.next/standalone/lib/qwen-sessions.ts +313 -0
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +5 -5
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +17 -4
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +10 -10
- package/.next/standalone/package.json +11 -11
- package/.next/standalone/sdk/python/uv.lock +29 -29
- package/.next/standalone/server.js +1 -1
- package/README.md +38 -7
- package/bin/failproofai.mjs +24 -7
- package/dist/cli.mjs +20313 -17756
- package/dist/worker.mjs +8521 -6768
- package/lib/cli-registry.ts +33 -1
- package/lib/cline-projects.ts +155 -0
- package/lib/cline-sessions.ts +268 -0
- package/lib/download-session.ts +37 -0
- package/lib/grok-projects.ts +123 -0
- package/lib/grok-sessions.ts +355 -0
- package/lib/ori-projects.ts +141 -0
- package/lib/ori-sessions.ts +221 -0
- package/lib/projects.ts +27 -3
- package/lib/qwen-projects.ts +106 -0
- package/lib/qwen-sessions.ts +313 -0
- package/package.json +11 -11
- package/src/audit/cli-adapters/cline.ts +50 -0
- package/src/audit/cli-adapters/grok.ts +70 -0
- package/src/audit/cli-adapters/index.ts +24 -0
- package/src/audit/cli-adapters/ori.ts +55 -0
- package/src/audit/cli-adapters/qwen.ts +71 -0
- package/src/hooks/batch-expand.ts +380 -0
- package/src/hooks/batch-fanout.ts +142 -0
- package/src/hooks/builtin-policies.ts +6 -4
- package/src/hooks/enforcement-capability.ts +97 -0
- package/src/hooks/handler.ts +61 -5
- package/src/hooks/harness-cli.ts +3 -1
- package/src/hooks/integrations.ts +756 -2
- package/src/hooks/normalize-cli-payload.ts +126 -0
- package/src/hooks/policy-evaluator.ts +297 -6
- package/src/hooks/resolve-transcript-path.ts +10 -0
- package/src/hooks/risk-patterns.ts +26 -0
- package/src/hooks/tool-name-canonicalize.ts +64 -0
- package/src/hooks/types.ts +533 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1le9jqc._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1p-qi2t._.js +0 -5
- package/.next/standalone/.next/server/chunks/_1-akg6w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00-s7h8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__190won8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1sc84bb._.js +0 -3
- package/.next/standalone/.next/static/chunks/03slu0c8_71y7.js +0 -1
- package/.next/standalone/.next/static/chunks/1-_zkmnm-jwei.js +0 -1
- package/.next/standalone/.next/static/chunks/1r9676mkvihto.js +0 -1
- package/.next/standalone/.next/static/chunks/30pct5s734jua.js +0 -6
- package/.next/standalone/.next/static/chunks/40nr6ehj4p3cw.css +0 -2
- package/.next/standalone/.next/static/chunks/41b5ptaocvi_l.js +0 -1
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{JBYgkXkA1PS48m9P_hKNw → qJx3mbOQV-5ElrcRb4Sp7}/_clientMiddlewareManifest.js +0 -0
- /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
|
+
);
|