failproofai 0.0.13 → 0.0.14-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.factory/hooks.json +112 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- 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 +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- 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/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- 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 +3 -3
- package/.next/standalone/.next/server/app/api/audit/run/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/reminder/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 +2 -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/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 +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +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/server-reference-manifest.json +8 -8
- 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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -2
- 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/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 +5 -3
- 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/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/chunks/[root-of-the-server]__0--lkk6._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jllheg._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1d9qen3._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +3 -0
- package/.next/standalone/.next/server/chunks/{_1z062h5._.js → _1vslkoe._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_factory-projects_ts_1eo_bk-._.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_openclaw-projects_ts_0gc-p24._.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +24 -12
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07iom4f._.js → [root-of-the-server]__07qfsfa._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ge8e7w._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h12me5._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11rtg6s._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +12 -9
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dky4g0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__212nf49._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0_9_ytf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0ul5rf_._.js → _1_ozgif._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- 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/lib_0xkhw_v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_antigravity-projects_ts_0_6zf11._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_devin-projects_ts_0a64qdy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_factory-projects_ts_1uyaahq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_goose-projects_ts_0zkscri._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_openclaw-projects_ts_0qxq3bp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_13p7zob.js → node_modules_html-to-image_es_index_0g5r64e.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0-uvagv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03c7gi5.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_09p-8om.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0v-kfiu.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0xrgzyz.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1806lsc.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1j6dd-e.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1sa65r-.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- 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 +10 -10
- package/.next/standalone/.next/static/chunks/{23c7v2svx90dp.js → 003uz088wbmz7.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0vc0eak148vo6.js → 0405htbxay9vd.js} +1 -1
- package/.next/standalone/.next/static/chunks/0iinhn4417nk1.js +1 -0
- package/.next/standalone/.next/static/chunks/1374x9bv_s_vg.js +6 -0
- package/.next/standalone/.next/static/chunks/{30ysv-_8jj052.js → 17sdztgbcc0fi.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2cejn75gug3j8.js → 2m_tbl39fcedn.js} +1 -1
- package/.next/standalone/.next/static/chunks/{36wdtkgdf0zrd.js → 2o3ql0n5owqsw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0wtagd8a5gwq-.js → 2ufuwhv03yvtb.js} +1 -1
- package/.next/standalone/.next/static/chunks/35-90m9py64d4.js +1 -0
- package/.next/standalone/.next/static/chunks/3r--yyjkp9_7b.css +2 -0
- package/.next/standalone/.next/static/chunks/43weu8f59tlix.js +1 -0
- package/.next/standalone/.next/static/chunks/{05apzuand0c22.js → 454dyizqik6t9.js} +2 -2
- package/.next/standalone/app/api/audit/run/route.ts +2 -0
- package/.next/standalone/app/audit/_components/empty-state.tsx +1 -1
- package/.next/standalone/app/globals.css +25 -1
- package/.next/standalone/app/policies/hooks-client.tsx +2 -4
- package/.next/standalone/app/project/[name]/page.tsx +30 -10
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +71 -21
- package/.next/standalone/assets/logos/antigravity.svg +1 -0
- package/.next/standalone/assets/logos/devin.svg +1 -0
- package/.next/standalone/assets/logos/factory-dark.png +0 -0
- package/.next/standalone/assets/logos/factory-light.png +0 -0
- package/.next/standalone/assets/logos/goose-dark.svg +1 -0
- package/.next/standalone/assets/logos/goose-light.svg +1 -0
- package/.next/standalone/assets/logos/openclaw.svg +22 -0
- package/.next/standalone/components/navbar.tsx +5 -11
- package/.next/standalone/components/reach-developers.tsx +1 -1
- package/.next/standalone/lib/antigravity-projects.ts +224 -0
- package/.next/standalone/lib/antigravity-sessions.ts +283 -0
- package/.next/standalone/lib/cli-registry.ts +26 -6
- package/.next/standalone/lib/devin-projects.ts +141 -0
- package/.next/standalone/lib/devin-sessions.ts +343 -0
- package/.next/standalone/lib/download-session.ts +56 -5
- package/.next/standalone/lib/factory-projects.ts +109 -0
- package/.next/standalone/lib/factory-sessions.ts +312 -0
- package/.next/standalone/lib/goose-projects.ts +148 -0
- package/.next/standalone/lib/goose-sessions.ts +296 -0
- package/.next/standalone/lib/openclaw-projects.ts +182 -0
- package/.next/standalone/lib/openclaw-sessions.ts +284 -0
- package/.next/standalone/lib/projects.ts +33 -9
- package/.next/standalone/lib/telemetry-sanitize.ts +43 -0
- package/.next/standalone/openclaw-plugin/index.js +223 -0
- package/.next/standalone/openclaw-plugin/openclaw.plugin.json +6 -0
- package/.next/standalone/openclaw-plugin/package.json +12 -0
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/server.js +1 -1
- package/README.md +105 -57
- package/bin/failproofai.mjs +87 -29
- package/dist/cli.mjs +12803 -9809
- package/lib/antigravity-projects.ts +224 -0
- package/lib/antigravity-sessions.ts +283 -0
- package/lib/cli-registry.ts +26 -6
- package/lib/devin-projects.ts +141 -0
- package/lib/devin-sessions.ts +343 -0
- package/lib/download-session.ts +56 -5
- package/lib/factory-projects.ts +109 -0
- package/lib/factory-sessions.ts +312 -0
- package/lib/goose-projects.ts +148 -0
- package/lib/goose-sessions.ts +296 -0
- package/lib/openclaw-projects.ts +182 -0
- package/lib/openclaw-sessions.ts +284 -0
- package/lib/projects.ts +33 -9
- package/lib/telemetry-sanitize.ts +43 -0
- package/openclaw-plugin/index.js +223 -0
- package/openclaw-plugin/openclaw.plugin.json +6 -0
- package/openclaw-plugin/package.json +12 -0
- package/package.json +2 -1
- package/scripts/launch.ts +4 -8
- package/scripts/postinstall.mjs +3 -4
- package/scripts/sync-agent-cli-harnesses-prompt.md +7 -10
- package/scripts/translate-docs/cli.ts +73 -52
- package/scripts/translate-docs/config.ts +57 -13
- package/scripts/translate-docs/mdx-translator.ts +51 -31
- package/scripts/translate-docs/mintlify-nav.ts +91 -10
- package/scripts/translate-docs/types.ts +3 -0
- package/src/audit/cli-adapters/antigravity.ts +87 -0
- package/src/audit/cli-adapters/devin.ts +53 -0
- package/src/audit/cli-adapters/factory.ts +75 -0
- package/src/audit/cli-adapters/goose.ts +53 -0
- package/src/audit/cli-adapters/index.ts +30 -6
- package/src/audit/cli-adapters/openclaw.ts +56 -0
- package/src/audit/cli.ts +44 -1
- package/src/audit/report.ts +1 -1
- package/src/audit/types.ts +1 -1
- package/src/hooks/builtin-policies.ts +5 -5
- package/src/hooks/configure-wizard.ts +356 -0
- package/src/hooks/handler.ts +283 -218
- package/src/hooks/hook-telemetry.ts +43 -3
- package/src/hooks/install-prompt.ts +92 -173
- package/src/hooks/integrations.ts +739 -138
- package/src/hooks/manager.ts +46 -2
- package/src/hooks/policy-evaluator.ts +301 -121
- package/src/hooks/policy-presets.ts +71 -0
- package/src/hooks/resolve-cwd.ts +1 -1
- package/src/hooks/resolve-permission-mode.ts +1 -5
- package/src/hooks/resolve-transcript-path.ts +25 -7
- package/src/hooks/tool-name-canonicalize.ts +36 -3
- package/src/hooks/tui.ts +578 -0
- package/src/hooks/types.ts +447 -109
- package/.next/standalone/.gemini/settings.json +0 -147
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cag8qd._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cefjd9._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ekj92q._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12v-lma._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__136y39g._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h8pzr._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hgv_75._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04bqvos._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dqgs96._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q5rfxg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qukut7._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__108slqn._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ql-mch._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_11v0g3z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_1c7bgx-._.js +0 -3
- package/.next/standalone/.next/static/chunks/0fa7j157kg9_d.js +0 -1
- package/.next/standalone/.next/static/chunks/0nrah-qv6smeg.js +0 -6
- package/.next/standalone/.next/static/chunks/1c8i78p7t1i-5.js +0 -1
- package/.next/standalone/.next/static/chunks/25qr0e6ez0ei0.js +0 -1
- package/.next/standalone/.next/static/chunks/25s52un3k_wht.css +0 -2
- package/.next/standalone/assets/logos/gemini-dark.svg +0 -13
- package/.next/standalone/assets/logos/gemini-light.svg +0 -13
- package/.next/standalone/lib/gemini-projects.ts +0 -243
- package/.next/standalone/lib/gemini-sessions.ts +0 -365
- package/lib/gemini-projects.ts +0 -243
- package/lib/gemini-sessions.ts +0 -365
- package/src/audit/cli-adapters/gemini.ts +0 -51
- package/src/hooks/first-run-nudge.ts +0 -146
- /package/.next/standalone/.next/static/{kmzdjTLVs6LqsfyOaUf69 → MNJWFb2jLRrxRKGFNK6Hy}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{kmzdjTLVs6LqsfyOaUf69 → MNJWFb2jLRrxRKGFNK6Hy}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{kmzdjTLVs6LqsfyOaUf69 → MNJWFb2jLRrxRKGFNK6Hy}/_ssgManifest.js +0 -0
package/lib/gemini-projects.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gemini CLI project discovery.
|
|
3
|
-
*
|
|
4
|
-
* Empirically verified against gemini-cli v0.40.1:
|
|
5
|
-
*
|
|
6
|
-
* • Session-state root: `~/.gemini/tmp/<project-basename>/`. Each subdir
|
|
7
|
-
* corresponds to one project. The basename is the cwd's last path segment
|
|
8
|
-
* (lossy when two projects share a basename — but every dir carries a
|
|
9
|
-
* `.project_root` text file with the absolute cwd to disambiguate).
|
|
10
|
-
* • Project list registry: `~/.gemini/projects.json` maps absolute cwd →
|
|
11
|
-
* basename. Authoritative when present, but we read each `.project_root`
|
|
12
|
-
* anyway so the dashboard tolerates partially-pruned registries.
|
|
13
|
-
* • Per-session file: `~/.gemini/tmp/<project>/chats/session-<ISO-timestamp>-<uuid-prefix>.jsonl`.
|
|
14
|
-
* A sidecar `<file>.jsonl.tool-calls.json` may sit alongside.
|
|
15
|
-
* • File format: JSONL. First line is metadata
|
|
16
|
-
* `{sessionId, projectHash, startTime, lastUpdated, kind}`; subsequent
|
|
17
|
-
* lines are message records `{id, timestamp, type, content: [{text}]}`
|
|
18
|
-
* and `{$set: {...}}` partial updates.
|
|
19
|
-
*
|
|
20
|
-
* As with Cursor / Pi / OpenCode, this module is intentionally permissive — a
|
|
21
|
-
* missing `~/.gemini/` returns `[]`, malformed JSONL falls open without
|
|
22
|
-
* surfacing the session.
|
|
23
|
-
*/
|
|
24
|
-
import { open, readdir, readFile, stat } from "node:fs/promises";
|
|
25
|
-
import { homedir } from "node:os";
|
|
26
|
-
import { join } from "node:path";
|
|
27
|
-
import type { ProjectFolder, SessionFile } from "./projects";
|
|
28
|
-
import { runtimeCache } from "./runtime-cache";
|
|
29
|
-
import { batchAll } from "./concurrency";
|
|
30
|
-
import { formatDate } from "./format-date";
|
|
31
|
-
import { encodeFolderName } from "./paths";
|
|
32
|
-
import { logWarn } from "./logger";
|
|
33
|
-
|
|
34
|
-
/** Filename pattern for a Gemini session JSONL:
|
|
35
|
-
* `session-<ISO-timestamp-with-dashes>-<8-hex-uuid-prefix>.jsonl`. */
|
|
36
|
-
const SESSION_FILE_RE = /^session-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2})-([0-9a-f]{8})\.jsonl$/i;
|
|
37
|
-
/** Full UUID — the filename only embeds the first 8 hex chars; the rest is on
|
|
38
|
-
* the JSONL metadata header line. The session detail route requires a full
|
|
39
|
-
* UUID, so links built from the truncated filename prefix 404. */
|
|
40
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
41
|
-
/** Metadata header sits on line 1 and is well under 1 KB; 4 KB covers it
|
|
42
|
-
* comfortably without slurping a multi-MB transcript. */
|
|
43
|
-
const FIRST_LINE_CHUNK_BYTES = 4 * 1024;
|
|
44
|
-
|
|
45
|
-
/** Override for tests. Defaults to the live Gemini session-state root. */
|
|
46
|
-
function getGeminiTmpRoot(): string {
|
|
47
|
-
return process.env.GEMINI_SESSIONS_DIR
|
|
48
|
-
|| join(homedir(), ".gemini", "tmp");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface GeminiSessionMeta {
|
|
52
|
-
filePath: string;
|
|
53
|
-
sessionFilename: string;
|
|
54
|
-
cwd: string;
|
|
55
|
-
fileMtime: Date;
|
|
56
|
-
/** Full UUID parsed from the JSONL metadata header (line 1). Undefined when
|
|
57
|
-
* the header is missing, malformed, or carries a non-UUID `sessionId`;
|
|
58
|
-
* callers fall through to rendering an un-linked row. */
|
|
59
|
-
sessionId?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function safeReaddir(dir: string) {
|
|
63
|
-
try {
|
|
64
|
-
return await readdir(dir, { withFileTypes: true });
|
|
65
|
-
} catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function statMtime(path: string): Promise<Date | null> {
|
|
71
|
-
try {
|
|
72
|
-
return (await stat(path)).mtime;
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Read the first newline-delimited line of `filePath` without slurping the
|
|
79
|
-
* rest. Mirrors `lib/codex-projects.ts`'s readFirstLine; the Gemini metadata
|
|
80
|
-
* header is always on line 1. */
|
|
81
|
-
async function readFirstLine(filePath: string): Promise<string | null> {
|
|
82
|
-
let fh: Awaited<ReturnType<typeof open>> | null = null;
|
|
83
|
-
try {
|
|
84
|
-
fh = await open(filePath, "r");
|
|
85
|
-
const buf = Buffer.alloc(FIRST_LINE_CHUNK_BYTES);
|
|
86
|
-
const { bytesRead } = await fh.read(buf, 0, FIRST_LINE_CHUNK_BYTES, 0);
|
|
87
|
-
if (bytesRead === 0) return null;
|
|
88
|
-
const slice = buf.subarray(0, bytesRead);
|
|
89
|
-
const nl = slice.indexOf(0x0a); // '\n'
|
|
90
|
-
const end = nl === -1 ? bytesRead : nl;
|
|
91
|
-
return slice.subarray(0, end).toString("utf-8");
|
|
92
|
-
} catch {
|
|
93
|
-
return null;
|
|
94
|
-
} finally {
|
|
95
|
-
if (fh) await fh.close().catch(() => {});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Extract a full-UUID `sessionId` from a JSONL metadata header line. Returns
|
|
100
|
-
* undefined on parse failure, missing field, or a non-UUID value. */
|
|
101
|
-
function extractFullSessionId(line: string | null): string | undefined {
|
|
102
|
-
if (!line) return undefined;
|
|
103
|
-
try {
|
|
104
|
-
const meta = JSON.parse(line) as { sessionId?: unknown };
|
|
105
|
-
if (typeof meta.sessionId !== "string") return undefined;
|
|
106
|
-
return UUID_RE.test(meta.sessionId) ? meta.sessionId : undefined;
|
|
107
|
-
} catch {
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Read `.project_root` to recover the absolute cwd for a basename folder.
|
|
113
|
-
* Returns null if missing or empty (caller treats the folder as un-mappable). */
|
|
114
|
-
async function readProjectRoot(projectDir: string): Promise<string | null> {
|
|
115
|
-
try {
|
|
116
|
-
const text = await readFile(join(projectDir, ".project_root"), "utf-8");
|
|
117
|
-
const trimmed = text.trim();
|
|
118
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
119
|
-
} catch {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function scanGeminiSessions(): Promise<GeminiSessionMeta[]> {
|
|
125
|
-
const root = getGeminiTmpRoot();
|
|
126
|
-
const projectDirs = await safeReaddir(root);
|
|
127
|
-
if (!projectDirs) return [];
|
|
128
|
-
|
|
129
|
-
const out: GeminiSessionMeta[] = [];
|
|
130
|
-
|
|
131
|
-
await batchAll(
|
|
132
|
-
projectDirs
|
|
133
|
-
.filter((d) => d.isDirectory())
|
|
134
|
-
.map((d) => async () => {
|
|
135
|
-
const projectDir = join(root, d.name);
|
|
136
|
-
const cwd = await readProjectRoot(projectDir);
|
|
137
|
-
if (!cwd) return;
|
|
138
|
-
|
|
139
|
-
const chatsDir = join(projectDir, "chats");
|
|
140
|
-
const files = await safeReaddir(chatsDir);
|
|
141
|
-
if (!files) return;
|
|
142
|
-
|
|
143
|
-
for (const f of files) {
|
|
144
|
-
if (!f.isFile()) continue;
|
|
145
|
-
if (!SESSION_FILE_RE.test(f.name)) continue;
|
|
146
|
-
const filePath = join(chatsDir, f.name);
|
|
147
|
-
const mtime = await statMtime(filePath);
|
|
148
|
-
if (!mtime) continue;
|
|
149
|
-
const sessionId = extractFullSessionId(await readFirstLine(filePath));
|
|
150
|
-
out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime, sessionId });
|
|
151
|
-
}
|
|
152
|
-
}),
|
|
153
|
-
16,
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
return out;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Returns one ProjectFolder per unique cwd that has at least one session file.
|
|
160
|
-
* `name` is the encoded full-cwd slug (`encodeFolderName(cwd)`), matching the
|
|
161
|
-
* routing scheme used by the dashboard's project URL — `mergeProjectFolders`
|
|
162
|
-
* unions by `name`, and `getGeminiSessionsByEncodedName` looks up by the same
|
|
163
|
-
* slug, so every cross-CLI merge and Gemini-only project link round-trips. */
|
|
164
|
-
export async function getGeminiProjects(): Promise<ProjectFolder[]> {
|
|
165
|
-
const sessions = await scanGeminiSessions();
|
|
166
|
-
const byCwd = new Map<string, { mtime: Date }>();
|
|
167
|
-
for (const s of sessions) {
|
|
168
|
-
const existing = byCwd.get(s.cwd);
|
|
169
|
-
if (!existing || s.fileMtime.getTime() > existing.mtime.getTime()) {
|
|
170
|
-
byCwd.set(s.cwd, { mtime: s.fileMtime });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const folders: ProjectFolder[] = [...byCwd.entries()].map(([cwd, { mtime }]) => ({
|
|
174
|
-
name: encodeFolderName(cwd),
|
|
175
|
-
path: cwd,
|
|
176
|
-
isDirectory: true,
|
|
177
|
-
lastModified: mtime,
|
|
178
|
-
lastModifiedFormatted: formatDate(mtime),
|
|
179
|
-
cli: ["gemini"],
|
|
180
|
-
}));
|
|
181
|
-
folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
182
|
-
return folders;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Returns SessionFile entries for the given absolute cwd. Empty if none. */
|
|
186
|
-
export async function getGeminiSessionsForCwd(cwd: string): Promise<SessionFile[]> {
|
|
187
|
-
const sessions = await scanGeminiSessions();
|
|
188
|
-
const matches = sessions.filter((s) => s.cwd === cwd);
|
|
189
|
-
const files: SessionFile[] = matches.map((s) => ({
|
|
190
|
-
name: s.sessionFilename,
|
|
191
|
-
path: s.filePath,
|
|
192
|
-
lastModified: s.fileMtime,
|
|
193
|
-
lastModifiedFormatted: formatDate(s.fileMtime),
|
|
194
|
-
sessionId: s.sessionId,
|
|
195
|
-
cli: "gemini",
|
|
196
|
-
}));
|
|
197
|
-
files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
198
|
-
return files;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export interface GeminiProjectByName {
|
|
202
|
-
cwd: string | null;
|
|
203
|
-
sessions: SessionFile[];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Looks up Gemini sessions for a project URL slug. `decodeFolderName` is lossy
|
|
208
|
-
* on cwds containing `-`, so we re-encode each session's cwd via
|
|
209
|
-
* `encodeFolderName` and match in that direction. Returns both the canonical
|
|
210
|
-
* cwd and the matching sessions. When two distinct cwds collapse to the same
|
|
211
|
-
* encoded slug we return null to avoid mis-labeling the project.
|
|
212
|
-
*/
|
|
213
|
-
export async function getGeminiSessionsByEncodedName(name: string): Promise<GeminiProjectByName> {
|
|
214
|
-
let metas: GeminiSessionMeta[];
|
|
215
|
-
try {
|
|
216
|
-
metas = await scanGeminiSessions();
|
|
217
|
-
} catch (error) {
|
|
218
|
-
logWarn("Failed to scan Gemini sessions:", error);
|
|
219
|
-
return { cwd: null, sessions: [] };
|
|
220
|
-
}
|
|
221
|
-
const matches = metas.filter((m) => encodeFolderName(m.cwd) === name);
|
|
222
|
-
const uniqueCwds = Array.from(new Set(matches.map((m) => m.cwd)));
|
|
223
|
-
if (uniqueCwds.length !== 1) {
|
|
224
|
-
return { cwd: null, sessions: [] };
|
|
225
|
-
}
|
|
226
|
-
const sessions = matches.map((s) => ({
|
|
227
|
-
name: s.sessionFilename,
|
|
228
|
-
path: s.filePath,
|
|
229
|
-
lastModified: s.fileMtime,
|
|
230
|
-
lastModifiedFormatted: formatDate(s.fileMtime),
|
|
231
|
-
sessionId: s.sessionId,
|
|
232
|
-
cli: "gemini" as const,
|
|
233
|
-
}));
|
|
234
|
-
sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
235
|
-
return { cwd: uniqueCwds[0], sessions };
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export const getCachedGeminiProjects = runtimeCache(getGeminiProjects, 30);
|
|
239
|
-
export const getCachedGeminiSessionsByEncodedName = runtimeCache(
|
|
240
|
-
(name: string) => getGeminiSessionsByEncodedName(name),
|
|
241
|
-
30,
|
|
242
|
-
{ maxSize: 50 },
|
|
243
|
-
);
|
package/lib/gemini-sessions.ts
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gemini CLI session transcript discovery + JSONL parser.
|
|
3
|
-
*
|
|
4
|
-
* Empirically verified against gemini-cli v0.40.1:
|
|
5
|
-
*
|
|
6
|
-
* Session files live at
|
|
7
|
-
* `~/.gemini/tmp/<project-basename>/chats/session-<ISO-timestamp-with-dashes>-<8-hex-uuid-prefix>.jsonl`
|
|
8
|
-
* with a sidecar `<file>.jsonl.tool-calls.json` of tool-call records.
|
|
9
|
-
* The basename component is just the cwd's last path segment; the canonical
|
|
10
|
-
* cwd lives at `~/.gemini/tmp/<project-basename>/.project_root`.
|
|
11
|
-
*
|
|
12
|
-
* JSONL record schema (observed):
|
|
13
|
-
* • Line 1 (metadata): `{sessionId, projectHash, startTime, lastUpdated, kind}`
|
|
14
|
-
* • Subsequent lines:
|
|
15
|
-
* `{id, timestamp, type: "user" | "assistant" | ..., content: [{text}]}` (messages)
|
|
16
|
-
* `{$set: {lastUpdated: "..."}}` (partial update)
|
|
17
|
-
*
|
|
18
|
-
* Parser is intentionally permissive — unknown record types degrade to
|
|
19
|
-
* "system" entries; malformed lines are skipped without aborting; and the
|
|
20
|
-
* loader fall-opens (returns null) on any I/O or parse failure.
|
|
21
|
-
*/
|
|
22
|
-
import { closeSync, openSync, readFileSync, readSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
23
|
-
import { readFile } from "node:fs/promises";
|
|
24
|
-
import { join, resolve, sep } from "node:path";
|
|
25
|
-
import { homedir } from "node:os";
|
|
26
|
-
import { runtimeCache } from "./runtime-cache";
|
|
27
|
-
import {
|
|
28
|
-
baseEntry,
|
|
29
|
-
type LogEntry,
|
|
30
|
-
type UserEntry,
|
|
31
|
-
type AssistantEntry,
|
|
32
|
-
type GenericEntry,
|
|
33
|
-
type QueueOperationEntry,
|
|
34
|
-
type ContentBlock,
|
|
35
|
-
type LogSource,
|
|
36
|
-
} from "./log-entries";
|
|
37
|
-
|
|
38
|
-
// ── Paths ──
|
|
39
|
-
|
|
40
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
41
|
-
/** Matches `session-<timestamp-of-arbitrary-shape>-<8-hex-uuid-prefix>.jsonl`.
|
|
42
|
-
* Empirically gemini-cli v0.40.1 emits minute-precision (`YYYY-MM-DDTHH-mm`),
|
|
43
|
-
* but the documented format includes seconds (`YYYY-MM-DDTHH-mm:ss`) and may
|
|
44
|
-
* evolve further. The first-line `sessionId` validation in findGeminiTranscript
|
|
45
|
-
* is the load-bearing safety check, so we accept any timestamp shape. */
|
|
46
|
-
const SESSION_FILE_RE = /^session-(.+)-([0-9a-f]{8})\.jsonl$/i;
|
|
47
|
-
|
|
48
|
-
/** Root directory for Gemini session state, honoring GEMINI_SESSIONS_DIR. */
|
|
49
|
-
export function getGeminiSessionStateRoot(): string {
|
|
50
|
-
return process.env.GEMINI_SESSIONS_DIR
|
|
51
|
-
|| join(homedir(), ".gemini", "tmp");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Reject a sessionId that isn't a UUID — defends against path traversal. */
|
|
55
|
-
function isSafeSessionId(sessionId: string): boolean {
|
|
56
|
-
return UUID_RE.test(sessionId);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Read up to the first newline from `path` without loading the whole file.
|
|
61
|
-
* Used by `findGeminiTranscript` to inspect the JSONL metadata header on
|
|
62
|
-
* candidate matches; the typical header is well under 1KB and we cap at 4KB
|
|
63
|
-
* for safety so a degenerate single-line file can't blow memory.
|
|
64
|
-
*
|
|
65
|
-
* Returns the first line as a UTF-8 string, or null on read failure / empty
|
|
66
|
-
* file. If the first 4KB contain no newline, returns whatever was read so the
|
|
67
|
-
* JSON.parse caller can still attempt — JSON.parse will fail for a truncated
|
|
68
|
-
* object, which findGeminiTranscript treats as a malformed-header miss.
|
|
69
|
-
*/
|
|
70
|
-
function readFirstLineSync(path: string): string | null {
|
|
71
|
-
let fd: number;
|
|
72
|
-
try {
|
|
73
|
-
fd = openSync(path, "r");
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const buf = Buffer.alloc(4096);
|
|
79
|
-
const bytesRead = readSync(fd, buf, 0, buf.length, 0);
|
|
80
|
-
if (bytesRead === 0) return null;
|
|
81
|
-
const text = buf.subarray(0, bytesRead).toString("utf-8");
|
|
82
|
-
const nl = text.indexOf("\n");
|
|
83
|
-
return nl >= 0 ? text.slice(0, nl) : text;
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
} finally {
|
|
87
|
-
try { closeSync(fd); } catch { /* best-effort */ }
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Find the JSONL transcript for `sessionId`. Walks every per-project subdir's
|
|
93
|
-
* `chats/` directory, matches the 8-hex prefix, and verifies the first record's
|
|
94
|
-
* full `sessionId` field matches. Rejects path-traversal sessionIds and
|
|
95
|
-
* verifies resolved paths stay under the root. Returns null on miss.
|
|
96
|
-
*/
|
|
97
|
-
export function findGeminiTranscript(sessionId: string): string | null {
|
|
98
|
-
if (!isSafeSessionId(sessionId)) return null;
|
|
99
|
-
const root = resolve(getGeminiSessionStateRoot());
|
|
100
|
-
const wantPrefix = sessionId.slice(0, 8).toLowerCase();
|
|
101
|
-
|
|
102
|
-
let projectDirs: string[];
|
|
103
|
-
try {
|
|
104
|
-
projectDirs = readdirSync(root);
|
|
105
|
-
} catch {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const projectDir of projectDirs) {
|
|
110
|
-
const projectPath = resolve(root, projectDir);
|
|
111
|
-
if (!projectPath.startsWith(`${root}${sep}`)) continue;
|
|
112
|
-
|
|
113
|
-
const chatsDir = resolve(projectPath, "chats");
|
|
114
|
-
let files: string[];
|
|
115
|
-
try {
|
|
116
|
-
files = readdirSync(chatsDir);
|
|
117
|
-
} catch {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
for (const f of files) {
|
|
122
|
-
const m = SESSION_FILE_RE.exec(f);
|
|
123
|
-
if (!m || m[2].toLowerCase() !== wantPrefix) continue;
|
|
124
|
-
const candidate = resolve(chatsDir, f);
|
|
125
|
-
if (!candidate.startsWith(`${chatsDir}${sep}`)) continue;
|
|
126
|
-
if (!existsSync(candidate)) continue;
|
|
127
|
-
// Confirm the full sessionId — the 8-hex prefix isn't unique on its own.
|
|
128
|
-
// Read only the first ~4KB so large transcripts don't bloat memory; the
|
|
129
|
-
// metadata header sits on line 1 well within that bound. The full file
|
|
130
|
-
// is re-read in getGeminiSessionLog() once we've matched.
|
|
131
|
-
const firstLine = readFirstLineSync(candidate);
|
|
132
|
-
if (!firstLine) continue;
|
|
133
|
-
try {
|
|
134
|
-
const meta = JSON.parse(firstLine) as { sessionId?: unknown };
|
|
135
|
-
if (typeof meta.sessionId === "string" && meta.sessionId.toLowerCase() === sessionId.toLowerCase()) {
|
|
136
|
-
return candidate;
|
|
137
|
-
}
|
|
138
|
-
} catch {
|
|
139
|
-
// Malformed first record — try next file.
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ── Parser ──
|
|
148
|
-
|
|
149
|
-
interface GeminiSessionRecord {
|
|
150
|
-
// Metadata-line fields (line 1)
|
|
151
|
-
sessionId?: string;
|
|
152
|
-
projectHash?: string;
|
|
153
|
-
startTime?: string;
|
|
154
|
-
lastUpdated?: string;
|
|
155
|
-
kind?: string;
|
|
156
|
-
// Message-line fields
|
|
157
|
-
id?: string;
|
|
158
|
-
timestamp?: string;
|
|
159
|
-
type?: string;
|
|
160
|
-
content?: Array<Record<string, unknown>>;
|
|
161
|
-
// Partial-update line: `{$set: {...}}`
|
|
162
|
-
$set?: Record<string, unknown>;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
interface GeminiParseResult {
|
|
166
|
-
entries: LogEntry[];
|
|
167
|
-
rawLines: Record<string, unknown>[];
|
|
168
|
-
/** Working directory pulled from the sidecar `.project_root` if available. */
|
|
169
|
-
cwd?: string;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function extractMessageText(content: Array<Record<string, unknown>> | undefined): string {
|
|
173
|
-
if (!Array.isArray(content)) return "";
|
|
174
|
-
const parts: string[] = [];
|
|
175
|
-
for (const block of content) {
|
|
176
|
-
if (typeof block?.text === "string") parts.push(block.text);
|
|
177
|
-
}
|
|
178
|
-
return parts.join("\n\n");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function buildAssistantContent(content: Array<Record<string, unknown>> | undefined): ContentBlock[] {
|
|
182
|
-
if (!Array.isArray(content)) return [];
|
|
183
|
-
const blocks: ContentBlock[] = [];
|
|
184
|
-
for (const block of content) {
|
|
185
|
-
if (typeof block?.text === "string" && block.text.length > 0) {
|
|
186
|
-
blocks.push({ type: "text", text: block.text });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
return blocks;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Parse a Gemini JSONL transcript into `LogEntry[]` plus the raw lines.
|
|
194
|
-
* Yields to the event loop every 200 lines so big transcripts don't block
|
|
195
|
-
* the request.
|
|
196
|
-
*/
|
|
197
|
-
export async function parseGeminiLog(
|
|
198
|
-
fileContent: string,
|
|
199
|
-
source: LogSource = "session",
|
|
200
|
-
cwdHint?: string,
|
|
201
|
-
): Promise<GeminiParseResult> {
|
|
202
|
-
const lines = fileContent.split("\n").filter((line) => line.trim() !== "");
|
|
203
|
-
const entries: LogEntry[] = [];
|
|
204
|
-
const rawLines: Record<string, unknown>[] = [];
|
|
205
|
-
let cwd: string | undefined = cwdHint;
|
|
206
|
-
let seenStart = false;
|
|
207
|
-
|
|
208
|
-
for (let i = 0; i < lines.length; i++) {
|
|
209
|
-
if (i > 0 && i % 200 === 0) await new Promise<void>((r) => setImmediate(r));
|
|
210
|
-
|
|
211
|
-
const line = lines[i];
|
|
212
|
-
let raw: GeminiSessionRecord;
|
|
213
|
-
try {
|
|
214
|
-
raw = JSON.parse(line) as GeminiSessionRecord;
|
|
215
|
-
} catch {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const rawCopy = { ...(raw as Record<string, unknown>), _source: source };
|
|
220
|
-
rawLines.push(rawCopy);
|
|
221
|
-
|
|
222
|
-
// Metadata line — derive a synthetic "Session Started" entry from startTime.
|
|
223
|
-
if (typeof raw.sessionId === "string" && typeof raw.startTime === "string") {
|
|
224
|
-
const date = new Date(raw.startTime);
|
|
225
|
-
if (!Number.isNaN(date.getTime())) {
|
|
226
|
-
const label: QueueOperationEntry["label"] = seenStart ? "Session Resumed" : "Session Started";
|
|
227
|
-
seenStart = true;
|
|
228
|
-
entries.push({
|
|
229
|
-
type: "queue-operation",
|
|
230
|
-
...baseEntry(rawCopy, date.toISOString(), date, source),
|
|
231
|
-
label,
|
|
232
|
-
} satisfies QueueOperationEntry);
|
|
233
|
-
}
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Partial-update line — preserve in raw but skip rendering.
|
|
238
|
-
if (raw.$set) continue;
|
|
239
|
-
|
|
240
|
-
const timestampStr = raw.timestamp;
|
|
241
|
-
if (!timestampStr) continue;
|
|
242
|
-
const date = new Date(timestampStr);
|
|
243
|
-
if (Number.isNaN(date.getTime())) continue;
|
|
244
|
-
const timestamp = date.toISOString();
|
|
245
|
-
|
|
246
|
-
const recType = raw.type;
|
|
247
|
-
|
|
248
|
-
if (recType === "user") {
|
|
249
|
-
const text = extractMessageText(raw.content);
|
|
250
|
-
if (!text) continue;
|
|
251
|
-
entries.push({
|
|
252
|
-
type: "user",
|
|
253
|
-
...baseEntry(rawCopy, timestamp, date, source),
|
|
254
|
-
message: { role: "user", content: text },
|
|
255
|
-
} satisfies UserEntry);
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (recType === "assistant" || recType === "model") {
|
|
260
|
-
const blocks = buildAssistantContent(raw.content);
|
|
261
|
-
if (blocks.length === 0) {
|
|
262
|
-
entries.push({
|
|
263
|
-
type: "system",
|
|
264
|
-
...baseEntry(rawCopy, timestamp, date, source),
|
|
265
|
-
raw: rawCopy,
|
|
266
|
-
} satisfies GenericEntry);
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
entries.push({
|
|
270
|
-
type: "assistant",
|
|
271
|
-
...baseEntry(rawCopy, timestamp, date, source),
|
|
272
|
-
message: { role: "assistant", content: blocks },
|
|
273
|
-
} satisfies AssistantEntry);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Unknown type — preserve raw.
|
|
278
|
-
entries.push({
|
|
279
|
-
type: "system",
|
|
280
|
-
...baseEntry(rawCopy, timestamp, date, source),
|
|
281
|
-
raw: rawCopy,
|
|
282
|
-
} satisfies GenericEntry);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (entries.length > 500) await new Promise<void>((r) => setImmediate(r));
|
|
286
|
-
entries.sort((a, b) => a.timestampMs - b.timestampMs);
|
|
287
|
-
|
|
288
|
-
return { entries, rawLines, cwd };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ── Public loader ──
|
|
292
|
-
|
|
293
|
-
export interface GeminiSessionLogData {
|
|
294
|
-
entries: LogEntry[];
|
|
295
|
-
rawLines: Record<string, unknown>[];
|
|
296
|
-
cwd?: string;
|
|
297
|
-
filePath: string;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** Read `.project_root` next to the chats dir to recover the absolute cwd. */
|
|
301
|
-
function readSiblingProjectRoot(transcriptPath: string): string | undefined {
|
|
302
|
-
// transcriptPath = .../<basename>/chats/<file>.jsonl
|
|
303
|
-
const chatsDir = resolve(transcriptPath, "..");
|
|
304
|
-
const projectDir = resolve(chatsDir, "..");
|
|
305
|
-
const rootFile = join(projectDir, ".project_root");
|
|
306
|
-
try {
|
|
307
|
-
const text = readFileSync(rootFile, "utf-8").trim();
|
|
308
|
-
return text.length > 0 ? text : undefined;
|
|
309
|
-
} catch {
|
|
310
|
-
return undefined;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export async function getGeminiSessionLog(sessionId: string): Promise<GeminiSessionLogData | null> {
|
|
315
|
-
const filePath = findGeminiTranscript(sessionId);
|
|
316
|
-
if (!filePath) return null;
|
|
317
|
-
let fileContent: string;
|
|
318
|
-
try {
|
|
319
|
-
fileContent = await readFile(filePath, "utf-8");
|
|
320
|
-
} catch {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
const cwdHint = readSiblingProjectRoot(filePath);
|
|
324
|
-
let parsed: GeminiParseResult;
|
|
325
|
-
try {
|
|
326
|
-
parsed = await parseGeminiLog(fileContent, "session", cwdHint);
|
|
327
|
-
} catch {
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
return {
|
|
331
|
-
entries: parsed.entries,
|
|
332
|
-
rawLines: parsed.rawLines,
|
|
333
|
-
cwd: parsed.cwd,
|
|
334
|
-
filePath,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export const getCachedGeminiSessionLog = runtimeCache(
|
|
339
|
-
(sessionId: string) => getGeminiSessionLog(sessionId),
|
|
340
|
-
60,
|
|
341
|
-
{ maxSize: 50 },
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
// ── Test helpers ──
|
|
345
|
-
|
|
346
|
-
export function _statGeminiTranscript(sessionId: string): { mtimeMs: number } | null {
|
|
347
|
-
const path = findGeminiTranscript(sessionId);
|
|
348
|
-
if (!path) return null;
|
|
349
|
-
try {
|
|
350
|
-
const s = statSync(path);
|
|
351
|
-
return { mtimeMs: s.mtimeMs };
|
|
352
|
-
} catch {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export function readGeminiTranscriptSync(sessionId: string): string | null {
|
|
358
|
-
const path = findGeminiTranscript(sessionId);
|
|
359
|
-
if (!path) return null;
|
|
360
|
-
try {
|
|
361
|
-
return readFileSync(path, "utf-8");
|
|
362
|
-
} catch {
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gemini CLI transcript adapter.
|
|
3
|
-
*/
|
|
4
|
-
import { statSync } from "node:fs";
|
|
5
|
-
import { getGeminiProjects, getGeminiSessionsByEncodedName } from "../../../lib/gemini-projects";
|
|
6
|
-
import { getGeminiSessionLog } from "../../../lib/gemini-sessions";
|
|
7
|
-
import type { NormalizedToolEvent, TranscriptMetadata } from "../types";
|
|
8
|
-
import type { ListOpts } from "./claude";
|
|
9
|
-
import { logEntriesToEvents } from "./shared";
|
|
10
|
-
|
|
11
|
-
export async function listGeminiTranscriptMetadata(
|
|
12
|
-
opts: ListOpts = {},
|
|
13
|
-
): Promise<TranscriptMetadata[]> {
|
|
14
|
-
const projectFilter = opts.projects ? new Set(opts.projects) : null;
|
|
15
|
-
const sinceMs = opts.sinceMs ?? 0;
|
|
16
|
-
const out: TranscriptMetadata[] = [];
|
|
17
|
-
|
|
18
|
-
const projects = await getGeminiProjects();
|
|
19
|
-
for (const project of projects) {
|
|
20
|
-
const { cwd, sessions } = await getGeminiSessionsByEncodedName(project.name);
|
|
21
|
-
const effectiveCwd = cwd ?? "";
|
|
22
|
-
if (projectFilter && !projectFilter.has(effectiveCwd)) continue;
|
|
23
|
-
for (const s of sessions) {
|
|
24
|
-
const mtimeMs = s.lastModified.getTime();
|
|
25
|
-
if (mtimeMs < sinceMs) continue;
|
|
26
|
-
let sizeBytes = 0;
|
|
27
|
-
try { sizeBytes = statSync(s.path).size; } catch { /* unreadable */ }
|
|
28
|
-
if (!s.sessionId) continue;
|
|
29
|
-
out.push({
|
|
30
|
-
cli: "gemini",
|
|
31
|
-
projectName: project.name,
|
|
32
|
-
sessionId: s.sessionId,
|
|
33
|
-
transcriptPath: s.path,
|
|
34
|
-
mtimeMs,
|
|
35
|
-
sizeBytes,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function streamGeminiEvents(meta: TranscriptMetadata): Promise<NormalizedToolEvent[]> {
|
|
43
|
-
const log = await getGeminiSessionLog(meta.sessionId);
|
|
44
|
-
if (!log) return [];
|
|
45
|
-
return logEntriesToEvents(log.entries, {
|
|
46
|
-
cli: "gemini",
|
|
47
|
-
sessionId: meta.sessionId,
|
|
48
|
-
transcriptPath: meta.transcriptPath,
|
|
49
|
-
cwd: log.cwd ?? "",
|
|
50
|
-
});
|
|
51
|
-
}
|