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
package/lib/cli-registry.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import type { IntegrationType } from "@/src/hooks/types";
|
|
28
28
|
|
|
29
29
|
/** Canonical CLI ids the registry knows about. Mirrors `INTEGRATION_TYPES`. */
|
|
30
|
-
export const KNOWN_CLI_IDS = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose"] as const satisfies readonly IntegrationType[];
|
|
30
|
+
export const KNOWN_CLI_IDS = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "hermes", "openclaw", "factory", "devin", "antigravity", "goose", "grok", "qwen", "ori", "cline"] as const satisfies readonly IntegrationType[];
|
|
31
31
|
export type CliId = (typeof KNOWN_CLI_IDS)[number];
|
|
32
32
|
|
|
33
33
|
/** Per-CLI metadata consumed by the dashboard. */
|
|
@@ -99,6 +99,38 @@ const CLI_ENTRIES: Record<CliId, CliEntry> = {
|
|
|
99
99
|
label: "Goose",
|
|
100
100
|
badgeClasses: "bg-lime-500/10 text-lime-400 border-lime-500/20",
|
|
101
101
|
},
|
|
102
|
+
grok: {
|
|
103
|
+
id: "grok",
|
|
104
|
+
label: "grok CLI",
|
|
105
|
+
// Neutral zinc, matching the design system's `--color-default`. Deliberately
|
|
106
|
+
// NOT a status hue: green/amber/red carry health meaning in this dashboard
|
|
107
|
+
// and must never be spent on identity. Neutral also happens to suit xAI's
|
|
108
|
+
// monochrome brand, and it is maximally far from qwen's magenta — the two
|
|
109
|
+
// were previously indistinguishable.
|
|
110
|
+
badgeClasses: "bg-zinc-500/10 text-zinc-200 border-zinc-500/20",
|
|
111
|
+
},
|
|
112
|
+
qwen: {
|
|
113
|
+
id: "qwen",
|
|
114
|
+
label: "Qwen Code",
|
|
115
|
+
// Magenta — the opposite pole from grok's neutral, and outside the
|
|
116
|
+
// status set (success/amber/orange/error) so it reads as identity.
|
|
117
|
+
badgeClasses: "bg-fuchsia-500/10 text-fuchsia-400 border-fuchsia-500/20",
|
|
118
|
+
},
|
|
119
|
+
ori: {
|
|
120
|
+
id: "ori",
|
|
121
|
+
label: "Ori",
|
|
122
|
+
// Sky — the badge set must be UNIQUE per CLI (a test asserts it), and
|
|
123
|
+
// violet/teal were already taken. Still outside the status set
|
|
124
|
+
// (success/amber/orange/error) so it reads as identity, not state.
|
|
125
|
+
badgeClasses: "bg-sky-500/10 text-sky-400 border-sky-500/20",
|
|
126
|
+
},
|
|
127
|
+
cline: {
|
|
128
|
+
id: "cline",
|
|
129
|
+
label: "Cline",
|
|
130
|
+
// Stone — the remaining neutral that is not zinc (grok's) and not a status
|
|
131
|
+
// colour.
|
|
132
|
+
badgeClasses: "bg-stone-500/10 text-stone-300 border-stone-500/20",
|
|
133
|
+
},
|
|
102
134
|
};
|
|
103
135
|
|
|
104
136
|
export function getCliEntry(id: string): CliEntry | undefined {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cline session enumeration — AUDIT-ONLY.
|
|
3
|
+
*
|
|
4
|
+
* One directory per session under `~/.cline/data/sessions/<epochMs>_<suffix>/`,
|
|
5
|
+
* each with a `<id>.json` metadata sidecar carrying a real `cwd`. So cline
|
|
6
|
+
* sessions group by project like Claude / Factory / Devin / Goose / ori rather
|
|
7
|
+
* than landing in one cwd-less bucket the way the Hermes gateway does.
|
|
8
|
+
*
|
|
9
|
+
* Enumeration reads only the small metadata sidecar, never the transcript — a
|
|
10
|
+
* project listing should not pay for parsing every message array on the box.
|
|
11
|
+
*/
|
|
12
|
+
import { readdir, stat } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
clineSessionsDir,
|
|
16
|
+
clineTimestampToMs,
|
|
17
|
+
readClineSessionMeta,
|
|
18
|
+
CLINE_SESSION_DIR_RE,
|
|
19
|
+
} from "./cline-sessions";
|
|
20
|
+
import { encodeFolderName } from "./paths";
|
|
21
|
+
import { runtimeCache } from "./runtime-cache";
|
|
22
|
+
import type { ProjectFolder, SessionFile } from "./projects";
|
|
23
|
+
import { formatDate } from "./format-date";
|
|
24
|
+
|
|
25
|
+
export interface ClineSessionRef {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
cwd?: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
provider?: string;
|
|
31
|
+
/** Encoded-cwd folder slug (matches Claude's `-home-user-project` scheme). */
|
|
32
|
+
projectName: string;
|
|
33
|
+
mtimeMs: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Truncate a first prompt into a session title the way the other adapters do. */
|
|
37
|
+
function titleFrom(prompt: string | undefined): string | undefined {
|
|
38
|
+
if (!prompt) return undefined;
|
|
39
|
+
// cline wraps the first turn in <user_input mode="act">…</user_input>.
|
|
40
|
+
const inner = /<user_input[^>]*>([\s\S]*?)<\/user_input>/.exec(prompt);
|
|
41
|
+
const text = (inner ? inner[1]! : prompt).trim().replace(/\s+/g, " ");
|
|
42
|
+
if (text.length === 0) return undefined;
|
|
43
|
+
return text.length > 60 ? `${text.slice(0, 59)}…` : text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* List every cline session. Returns `[]` when the directory is missing or
|
|
48
|
+
* unreadable (fail-open — the audit just skips cline).
|
|
49
|
+
*/
|
|
50
|
+
export async function getClineSessions(): Promise<ClineSessionRef[]> {
|
|
51
|
+
const root = clineSessionsDir();
|
|
52
|
+
let names: string[];
|
|
53
|
+
try {
|
|
54
|
+
names = await readdir(root);
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const out: ClineSessionRef[] = [];
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
if (!CLINE_SESSION_DIR_RE.test(name)) continue;
|
|
61
|
+
const dir = join(root, name);
|
|
62
|
+
const meta = await readClineSessionMeta(dir, name);
|
|
63
|
+
let mtimeMs =
|
|
64
|
+
clineTimestampToMs(meta?.ended_at) ||
|
|
65
|
+
clineTimestampToMs(meta?.started_at) ||
|
|
66
|
+
Number(name.split("_")[0]) ||
|
|
67
|
+
0;
|
|
68
|
+
if (mtimeMs === 0) {
|
|
69
|
+
try {
|
|
70
|
+
mtimeMs = (await stat(dir)).mtimeMs;
|
|
71
|
+
} catch {
|
|
72
|
+
/* leave 0 */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const cwd = meta?.cwd ?? meta?.workspace_root;
|
|
76
|
+
out.push({
|
|
77
|
+
sessionId: name,
|
|
78
|
+
cwd,
|
|
79
|
+
title: titleFrom(meta?.prompt),
|
|
80
|
+
model: meta?.model,
|
|
81
|
+
provider: meta?.provider,
|
|
82
|
+
projectName: cwd ? encodeFolderName(cwd) : "cline",
|
|
83
|
+
mtimeMs,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const getCachedClineSessions = runtimeCache(getClineSessions, 2);
|
|
91
|
+
|
|
92
|
+
// ── Dashboard history browser (projects list + project-detail sessions) ──
|
|
93
|
+
|
|
94
|
+
/** One `ProjectFolder` per encoded-cwd discovered in cline's session store. */
|
|
95
|
+
export async function getClineProjects(): Promise<ProjectFolder[]> {
|
|
96
|
+
const sessions = await getClineSessions();
|
|
97
|
+
const byName = new Map<string, { latest: number; cwd: string; name: string }>();
|
|
98
|
+
for (const s of sessions) {
|
|
99
|
+
if (!s.cwd) continue;
|
|
100
|
+
const existing = byName.get(s.projectName);
|
|
101
|
+
if (!existing || s.mtimeMs > existing.latest) {
|
|
102
|
+
byName.set(s.projectName, { latest: s.mtimeMs, cwd: s.cwd, name: s.projectName });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const folders: ProjectFolder[] = [];
|
|
106
|
+
for (const { name, cwd, latest } of byName.values()) {
|
|
107
|
+
const lastModified = new Date(latest);
|
|
108
|
+
folders.push({
|
|
109
|
+
name,
|
|
110
|
+
path: cwd,
|
|
111
|
+
isDirectory: true,
|
|
112
|
+
lastModified,
|
|
113
|
+
lastModifiedFormatted: formatDate(lastModified),
|
|
114
|
+
cli: ["cline"],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
118
|
+
return folders;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ClineProjectByName {
|
|
122
|
+
cwd: string | null;
|
|
123
|
+
sessions: SessionFile[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Resolve the cline sessions for a project URL slug (the encoded-cwd folder). */
|
|
127
|
+
export async function getClineSessionsByEncodedName(name: string): Promise<ClineProjectByName> {
|
|
128
|
+
const sessions = await getClineSessions();
|
|
129
|
+
const matched = sessions.filter((s) => s.cwd && s.projectName === name);
|
|
130
|
+
if (matched.length === 0) return { cwd: null, sessions: [] };
|
|
131
|
+
|
|
132
|
+
const sorted = [...matched].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
133
|
+
const cwd = sorted[0].cwd ?? null;
|
|
134
|
+
return {
|
|
135
|
+
cwd,
|
|
136
|
+
sessions: sorted.map((s) => {
|
|
137
|
+
const lastModified = new Date(s.mtimeMs);
|
|
138
|
+
return {
|
|
139
|
+
name: s.title ?? s.sessionId,
|
|
140
|
+
path: `cline-session://${s.sessionId}`,
|
|
141
|
+
lastModified,
|
|
142
|
+
lastModifiedFormatted: formatDate(lastModified),
|
|
143
|
+
sessionId: s.sessionId,
|
|
144
|
+
cli: "cline" as const,
|
|
145
|
+
};
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const getCachedClineProjects = runtimeCache(getClineProjects, 2);
|
|
151
|
+
export const getCachedClineSessionsByEncodedName = runtimeCache(
|
|
152
|
+
(name: string) => getClineSessionsByEncodedName(name),
|
|
153
|
+
2,
|
|
154
|
+
{ maxSize: 50 },
|
|
155
|
+
);
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cline session transcript loader + parser.
|
|
3
|
+
*
|
|
4
|
+
* AUDIT-ONLY (Pillar 2). Cline is the easiest transcript of any integration we
|
|
5
|
+
* support, because it already speaks CLAUDE'S OWN content-block schema. Each
|
|
6
|
+
* session is a directory under `~/.cline/data/sessions/<epochMs>_<suffix>/`
|
|
7
|
+
* holding exactly two files:
|
|
8
|
+
*
|
|
9
|
+
* <id>.json session metadata: {session_id, source, pid, started_at,
|
|
10
|
+
* ended_at, exit_code, status, interactive, provider,
|
|
11
|
+
* model, cwd, workspace_root, prompt}
|
|
12
|
+
* <id>.messages.json {version, updated_at, agent, sessionId, origin,
|
|
13
|
+
* system_prompt, messages}
|
|
14
|
+
*
|
|
15
|
+
* and `messages[].content` is an array of the very blocks lib/log-entries.ts
|
|
16
|
+
* already models: `{type:"thinking"}`, `{type:"text"}`,
|
|
17
|
+
* `{type:"tool_use", id, name, input}`, `{type:"tool_result", tool_use_id,
|
|
18
|
+
* name, content}`. So there is no dialect to translate — only tool_result
|
|
19
|
+
* pairing, which `clineMessagesToLogEntries` does by `tool_use_id`.
|
|
20
|
+
*
|
|
21
|
+
* Two things that are NOT free, recorded so nobody assumes otherwise:
|
|
22
|
+
* • Cline stores NO per-message timestamp. Every entry in a session therefore
|
|
23
|
+
* carries the session's own time, so ordering within a session is array
|
|
24
|
+
* order and per-message timing is unrecoverable. (Same limitation as ori.)
|
|
25
|
+
* • Tool RESULTS arrive in `role:"user"` messages, Claude-style. A user
|
|
26
|
+
* message whose content is entirely tool_result blocks is NOT user prose
|
|
27
|
+
* and must not be emitted as one, or every tool call produces a phantom
|
|
28
|
+
* user turn in the audit.
|
|
29
|
+
*
|
|
30
|
+
* Home override: `CLINE_HOME` (the dir containing `data/sessions`, used by
|
|
31
|
+
* tests); otherwise `$CLINE_DIR` — which cline itself honours — then `~/.cline`.
|
|
32
|
+
*/
|
|
33
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
34
|
+
import { homedir } from "node:os";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { runtimeCache } from "./runtime-cache";
|
|
37
|
+
import {
|
|
38
|
+
baseEntry,
|
|
39
|
+
formatTimestamp,
|
|
40
|
+
type LogEntry,
|
|
41
|
+
type UserEntry,
|
|
42
|
+
type AssistantEntry,
|
|
43
|
+
type ContentBlock,
|
|
44
|
+
type ToolUseBlock,
|
|
45
|
+
type LogSource,
|
|
46
|
+
} from "./log-entries";
|
|
47
|
+
import { formatDuration } from "./format-duration";
|
|
48
|
+
|
|
49
|
+
/** Absolute path to cline's home (override with CLINE_HOME, then CLINE_DIR). */
|
|
50
|
+
export function clineHome(): string {
|
|
51
|
+
return process.env.CLINE_HOME || process.env.CLINE_DIR || join(homedir(), ".cline");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Absolute path to the directory holding one folder per session. */
|
|
55
|
+
export function clineSessionsDir(): string {
|
|
56
|
+
return join(clineHome(), "data", "sessions");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Session directories are `<epochMs>_<suffix>`, e.g. `1788253271772_bn188`. */
|
|
60
|
+
export const CLINE_SESSION_DIR_RE = /^(\d{10,})_([A-Za-z0-9]+)$/;
|
|
61
|
+
|
|
62
|
+
export function clineTimestampToMs(value: string | null | undefined): number {
|
|
63
|
+
if (!value) return 0;
|
|
64
|
+
const ms = Date.parse(value);
|
|
65
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ClineSessionMeta {
|
|
69
|
+
session_id?: string;
|
|
70
|
+
status?: string;
|
|
71
|
+
provider?: string;
|
|
72
|
+
model?: string;
|
|
73
|
+
cwd?: string;
|
|
74
|
+
workspace_root?: string;
|
|
75
|
+
prompt?: string;
|
|
76
|
+
started_at?: string;
|
|
77
|
+
ended_at?: string;
|
|
78
|
+
source?: string;
|
|
79
|
+
interactive?: boolean;
|
|
80
|
+
exit_code?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface ClineBlock {
|
|
84
|
+
type?: string;
|
|
85
|
+
text?: string;
|
|
86
|
+
thinking?: string;
|
|
87
|
+
id?: string;
|
|
88
|
+
name?: string;
|
|
89
|
+
input?: Record<string, unknown>;
|
|
90
|
+
tool_use_id?: string;
|
|
91
|
+
content?: unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ClineMessage {
|
|
95
|
+
role?: string;
|
|
96
|
+
content?: string | ClineBlock[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Flatten a tool_result's `content` into display text. Cline uses an array of
|
|
100
|
+
* `{query, result}`-ish objects or plain text blocks depending on the tool, so
|
|
101
|
+
* fall back to JSON rather than dropping the result entirely. */
|
|
102
|
+
function toolResultText(content: unknown): string {
|
|
103
|
+
if (typeof content === "string") return content;
|
|
104
|
+
if (Array.isArray(content)) {
|
|
105
|
+
const parts = content.map((c) => {
|
|
106
|
+
if (typeof c === "string") return c;
|
|
107
|
+
if (c && typeof c === "object") {
|
|
108
|
+
const o = c as Record<string, unknown>;
|
|
109
|
+
if (typeof o.text === "string") return o.text;
|
|
110
|
+
if (typeof o.result === "string") return o.result;
|
|
111
|
+
return JSON.stringify(o);
|
|
112
|
+
}
|
|
113
|
+
return "";
|
|
114
|
+
});
|
|
115
|
+
return parts.filter((p) => p.length > 0).join("\n");
|
|
116
|
+
}
|
|
117
|
+
if (content && typeof content === "object") return JSON.stringify(content);
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse cline's message array into the shared LogEntry[] shape.
|
|
123
|
+
*
|
|
124
|
+
* PURE — takes already-parsed JSON, so tests need no fixture directory.
|
|
125
|
+
*/
|
|
126
|
+
export function clineMessagesToLogEntries(
|
|
127
|
+
messages: ClineMessage[],
|
|
128
|
+
sessionId: string,
|
|
129
|
+
timestampMs: number,
|
|
130
|
+
source: LogSource = "session",
|
|
131
|
+
): LogEntry[] {
|
|
132
|
+
const date = new Date(timestampMs);
|
|
133
|
+
const timestamp = date.toISOString();
|
|
134
|
+
const entries: LogEntry[] = [];
|
|
135
|
+
// tool_use id → the block awaiting its result, so a later user-role
|
|
136
|
+
// tool_result attaches onto the assistant turn that made the call.
|
|
137
|
+
const pending = new Map<string, ToolUseBlock>();
|
|
138
|
+
|
|
139
|
+
messages.forEach((msg, i) => {
|
|
140
|
+
const role = msg.role;
|
|
141
|
+
const raw = { uuid: `${sessionId}:${i}`, parentUuid: null } as Record<string, unknown>;
|
|
142
|
+
const base = baseEntry(raw, timestamp, date, source);
|
|
143
|
+
const blocks: ClineBlock[] = Array.isArray(msg.content) ? msg.content : [];
|
|
144
|
+
|
|
145
|
+
if (role === "user") {
|
|
146
|
+
// Attach any tool results this message carries…
|
|
147
|
+
let attached = 0;
|
|
148
|
+
for (const b of blocks) {
|
|
149
|
+
if (b.type !== "tool_result" || typeof b.tool_use_id !== "string") continue;
|
|
150
|
+
attached++;
|
|
151
|
+
const target = pending.get(b.tool_use_id);
|
|
152
|
+
if (!target) continue;
|
|
153
|
+
// No per-message timestamp exists, so duration is unknowable — 0 rather
|
|
154
|
+
// than a fabricated span.
|
|
155
|
+
target.result = {
|
|
156
|
+
timestamp,
|
|
157
|
+
timestampFormatted: formatTimestamp(date),
|
|
158
|
+
content: toolResultText(b.content),
|
|
159
|
+
durationMs: 0,
|
|
160
|
+
durationFormatted: formatDuration(0),
|
|
161
|
+
};
|
|
162
|
+
pending.delete(b.tool_use_id);
|
|
163
|
+
}
|
|
164
|
+
// …and only emit a user turn if there was real prose. A message that is
|
|
165
|
+
// nothing but tool results is the tool-result carrier, not a user turn.
|
|
166
|
+
const text =
|
|
167
|
+
typeof msg.content === "string"
|
|
168
|
+
? msg.content
|
|
169
|
+
: blocks
|
|
170
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
171
|
+
.map((b) => b.text as string)
|
|
172
|
+
.join("");
|
|
173
|
+
if (text.trim().length > 0) {
|
|
174
|
+
entries.push({ ...base, type: "user", message: { role: "user", content: text } } as UserEntry);
|
|
175
|
+
} else if (attached === 0 && typeof msg.content === "string") {
|
|
176
|
+
entries.push({ ...base, type: "user", message: { role: "user", content: msg.content } } as UserEntry);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (role === "assistant") {
|
|
182
|
+
const out: ContentBlock[] = [];
|
|
183
|
+
for (const b of blocks) {
|
|
184
|
+
if (b.type === "thinking" && typeof b.thinking === "string") {
|
|
185
|
+
out.push({ type: "thinking", thinking: b.thinking });
|
|
186
|
+
} else if (b.type === "text" && typeof b.text === "string") {
|
|
187
|
+
out.push({ type: "text", text: b.text });
|
|
188
|
+
} else if (b.type === "tool_use" && typeof b.id === "string") {
|
|
189
|
+
const block: ToolUseBlock = {
|
|
190
|
+
type: "tool_use",
|
|
191
|
+
id: b.id,
|
|
192
|
+
name: b.name ?? "",
|
|
193
|
+
input: b.input ?? {},
|
|
194
|
+
};
|
|
195
|
+
pending.set(b.id, block);
|
|
196
|
+
out.push(block);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (out.length > 0) {
|
|
200
|
+
entries.push({
|
|
201
|
+
...base,
|
|
202
|
+
type: "assistant",
|
|
203
|
+
message: { role: "assistant", content: out },
|
|
204
|
+
} as AssistantEntry);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return entries;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ClineSessionLog {
|
|
213
|
+
entries: LogEntry[];
|
|
214
|
+
cwd?: string;
|
|
215
|
+
meta?: ClineSessionMeta;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Read a session's metadata sidecar. Returns undefined when absent/unreadable. */
|
|
219
|
+
export async function readClineSessionMeta(
|
|
220
|
+
dir: string,
|
|
221
|
+
sessionId: string,
|
|
222
|
+
): Promise<ClineSessionMeta | undefined> {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(await readFile(join(dir, `${sessionId}.json`), "utf8")) as ClineSessionMeta;
|
|
225
|
+
} catch {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Load and parse one cline session. Returns null when absent or unreadable. */
|
|
231
|
+
export async function getClineSessionLog(sessionId: string): Promise<ClineSessionLog | null> {
|
|
232
|
+
const root = clineSessionsDir();
|
|
233
|
+
let dirs: string[];
|
|
234
|
+
try {
|
|
235
|
+
dirs = await readdir(root);
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
// The directory is `<epochMs>_<suffix>` and the files inside are named for
|
|
240
|
+
// the FULL directory name, which is also the session id.
|
|
241
|
+
const dirName = dirs.find((d) => d === sessionId);
|
|
242
|
+
if (!dirName) return null;
|
|
243
|
+
const dir = join(root, dirName);
|
|
244
|
+
|
|
245
|
+
let parsed: { messages?: ClineMessage[]; updated_at?: string };
|
|
246
|
+
try {
|
|
247
|
+
parsed = JSON.parse(await readFile(join(dir, `${sessionId}.messages.json`), "utf8")) as {
|
|
248
|
+
messages?: ClineMessage[];
|
|
249
|
+
updated_at?: string;
|
|
250
|
+
};
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const meta = await readClineSessionMeta(dir, sessionId);
|
|
255
|
+
const ms =
|
|
256
|
+
clineTimestampToMs(meta?.started_at) ||
|
|
257
|
+
clineTimestampToMs(parsed.updated_at) ||
|
|
258
|
+
Number(sessionId.split("_")[0]) ||
|
|
259
|
+
0;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
entries: clineMessagesToLogEntries(parsed.messages ?? [], sessionId, ms),
|
|
263
|
+
cwd: meta?.cwd ?? meta?.workspace_root,
|
|
264
|
+
meta,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const getCachedClineSessionLog = runtimeCache(getClineSessionLog, 2);
|
package/lib/download-session.ts
CHANGED
|
@@ -125,6 +125,21 @@ export async function resolveDownloadSource(
|
|
|
125
125
|
return path ? { kind: "file", path } : null;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
if (cli === "grok") {
|
|
129
|
+
// grok writes a real chat_history.jsonl per session directory — stream it
|
|
130
|
+
// verbatim (no synthesis, like Factory).
|
|
131
|
+
const { findGrokTranscript } = await import("./grok-sessions");
|
|
132
|
+
const path = findGrokTranscript(sessionId);
|
|
133
|
+
return path ? { kind: "file", path } : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (cli === "qwen") {
|
|
137
|
+
// qwen writes a real JSONL transcript per session — stream it verbatim.
|
|
138
|
+
const { findQwenTranscript } = await import("./qwen-sessions");
|
|
139
|
+
const path = findQwenTranscript(sessionId);
|
|
140
|
+
return path ? { kind: "file", path } : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
if (cli === "devin") {
|
|
129
144
|
// Devin keeps sessions in SQLite (~/.local/share/devin/cli/sessions.db).
|
|
130
145
|
// Synthesize a JSONL export of the session's raw chat_message rows.
|
|
@@ -152,6 +167,28 @@ export async function resolveDownloadSource(
|
|
|
152
167
|
return { kind: "synthesized", body, contentType: "application/x-ndjson", extension: "jsonl" };
|
|
153
168
|
}
|
|
154
169
|
|
|
170
|
+
if (cli === "ori") {
|
|
171
|
+
// Ori keeps the whole conversation in one SQLite column
|
|
172
|
+
// (ori_agent_loop_history.prompt). Synthesize a JSONL export from the
|
|
173
|
+
// parsed entries — there is no per-session file to stream.
|
|
174
|
+
const { getOriSessionLog } = await import("./ori-sessions");
|
|
175
|
+
const result = await getOriSessionLog(sessionId);
|
|
176
|
+
if (!result) return null;
|
|
177
|
+
const body = result.entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
178
|
+
return { kind: "synthesized", body, contentType: "application/x-ndjson", extension: "jsonl" };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (cli === "cline") {
|
|
182
|
+
// Cline stores a real per-session messages file, but it is a single JSON
|
|
183
|
+
// document rather than JSONL, so stream the parsed entries as JSONL for
|
|
184
|
+
// consistency with every other synthesized export.
|
|
185
|
+
const { getClineSessionLog } = await import("./cline-sessions");
|
|
186
|
+
const result = await getClineSessionLog(sessionId);
|
|
187
|
+
if (!result) return null;
|
|
188
|
+
const body = result.entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
189
|
+
return { kind: "synthesized", body, contentType: "application/x-ndjson", extension: "jsonl" };
|
|
190
|
+
}
|
|
191
|
+
|
|
155
192
|
// Exhaustive — but TypeScript can't always see CliId is exhausted across the
|
|
156
193
|
// if-chain above, so guard with a runtime fallback.
|
|
157
194
|
const _exhaustive: never = cli;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grok CLI project discovery.
|
|
3
|
+
*
|
|
4
|
+
* grok stores transcripts at
|
|
5
|
+
* `~/.grok/sessions/<percent-encoded-cwd>/<uuid>/chat_history.jsonl`.
|
|
6
|
+
*
|
|
7
|
+
* NOTE the folder encoding differs from every other JSONL store we read: grok
|
|
8
|
+
* percent-encodes the cwd (`%2Fhome%2Fuser%2Frepo`) where Claude, Factory and
|
|
9
|
+
* Qwen dash-encode it (`-home-user-repo`).
|
|
10
|
+
*
|
|
11
|
+
* So the on-disk folder name is deliberately NOT used as the project `name`.
|
|
12
|
+
* Two reasons, and the first is a hard bug: `name` becomes the URL slug for
|
|
13
|
+
* `/project/[name]`, and a percent-encoded name re-encodes to `%252F…` in the
|
|
14
|
+
* link, which the route cannot resolve — every grok project 404'd. Second, a
|
|
15
|
+
* name no other CLI can produce merges with nothing, so a cwd driven by both
|
|
16
|
+
* grok and Claude showed up as two unrelated rows.
|
|
17
|
+
*
|
|
18
|
+
* Re-encoding the decoded cwd with `encodeFolderName` fixes both at once: the
|
|
19
|
+
* slug is URL-safe, and it is byte-identical to the one Claude/Factory/Qwen
|
|
20
|
+
* derive for the same cwd, so those rows merge as they should.
|
|
21
|
+
*/
|
|
22
|
+
import { encodeFolderName, decodeFolderName } from "./paths";
|
|
23
|
+
import { listGrokTranscripts, getGrokSessionLog } from "./grok-sessions";
|
|
24
|
+
import type { ProjectFolder, SessionFile } from "./projects";
|
|
25
|
+
import { runtimeCache } from "./runtime-cache";
|
|
26
|
+
import { formatDate } from "./format-date";
|
|
27
|
+
import { logWarn } from "./logger";
|
|
28
|
+
|
|
29
|
+
/** Returns one ProjectFolder per percent-encoded cwd folder discovered under
|
|
30
|
+
* ~/.grok/sessions/. */
|
|
31
|
+
export async function getGrokProjects(): Promise<ProjectFolder[]> {
|
|
32
|
+
let transcripts;
|
|
33
|
+
try {
|
|
34
|
+
transcripts = listGrokTranscripts();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logWarn("Failed to scan grok sessions:", error);
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const byName = new Map<string, { latest: number; cwd: string; name: string }>();
|
|
41
|
+
for (const t of transcripts) {
|
|
42
|
+
// The URL slug is derived from the real cwd, never from grok's
|
|
43
|
+
// percent-encoded folder — see the module header.
|
|
44
|
+
const name = encodeFolderName(t.cwd);
|
|
45
|
+
const existing = byName.get(name);
|
|
46
|
+
if (!existing || t.mtimeMs > existing.latest) {
|
|
47
|
+
byName.set(name, { latest: t.mtimeMs, cwd: t.cwd, name });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const folders: ProjectFolder[] = [];
|
|
52
|
+
for (const { name, cwd, latest } of byName.values()) {
|
|
53
|
+
const lastModified = new Date(latest);
|
|
54
|
+
folders.push({
|
|
55
|
+
name,
|
|
56
|
+
path: cwd,
|
|
57
|
+
isDirectory: true,
|
|
58
|
+
lastModified,
|
|
59
|
+
lastModifiedFormatted: formatDate(lastModified),
|
|
60
|
+
cli: ["grok"],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
64
|
+
return folders;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface GrokProjectByName {
|
|
68
|
+
/** Canonical cwd, taken from a session's summary.json `info.cwd`. Null when
|
|
69
|
+
* no session could be read. */
|
|
70
|
+
cwd: string | null;
|
|
71
|
+
sessions: SessionFile[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Look up grok sessions for a project URL slug (the percent-encoded cwd folder
|
|
76
|
+
* name). The canonical cwd comes from summary.json; the percent-decode is the
|
|
77
|
+
* fallback.
|
|
78
|
+
*/
|
|
79
|
+
export async function getGrokSessionsByEncodedName(name: string): Promise<GrokProjectByName> {
|
|
80
|
+
let transcripts;
|
|
81
|
+
try {
|
|
82
|
+
// Match on the derived slug, since that is what the link carried.
|
|
83
|
+
transcripts = listGrokTranscripts().filter((t) => encodeFolderName(t.cwd) === name);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logWarn("Failed to scan grok sessions:", error);
|
|
86
|
+
return { cwd: null, sessions: [] };
|
|
87
|
+
}
|
|
88
|
+
if (transcripts.length === 0) return { cwd: null, sessions: [] };
|
|
89
|
+
|
|
90
|
+
const sorted = [...transcripts].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
91
|
+
|
|
92
|
+
let cwd: string | null = sorted[0].cwd || null;
|
|
93
|
+
if (!cwd) {
|
|
94
|
+
try {
|
|
95
|
+
const log = await getGrokSessionLog(sorted[0].sessionId);
|
|
96
|
+
cwd = log?.cwd ?? null;
|
|
97
|
+
} catch {
|
|
98
|
+
// best-effort — fall back to the decode below
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// `name` is a dash-encoded cwd here, so the shared decoder is the right one.
|
|
102
|
+
if (!cwd) cwd = decodeFolderName(name);
|
|
103
|
+
|
|
104
|
+
const sessions: SessionFile[] = sorted.map((t) => {
|
|
105
|
+
const lastModified = new Date(t.mtimeMs);
|
|
106
|
+
return {
|
|
107
|
+
name: t.sessionId,
|
|
108
|
+
path: t.transcriptPath,
|
|
109
|
+
lastModified,
|
|
110
|
+
lastModifiedFormatted: formatDate(lastModified),
|
|
111
|
+
sessionId: t.sessionId,
|
|
112
|
+
cli: "grok" as const,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return { cwd, sessions };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const getCachedGrokProjects = runtimeCache(getGrokProjects, 30);
|
|
119
|
+
export const getCachedGrokSessionsByEncodedName = runtimeCache(
|
|
120
|
+
(name: string) => getGrokSessionsByEncodedName(name),
|
|
121
|
+
30,
|
|
122
|
+
{ maxSize: 50 },
|
|
123
|
+
);
|