failproofai 0.0.9-beta.2 → 0.0.10-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.next/standalone/.cursor/hooks.json +47 -0
  2. package/.next/standalone/.gemini/settings.json +147 -0
  3. package/.next/standalone/.next/BUILD_ID +1 -1
  4. package/.next/standalone/.next/build-manifest.json +3 -3
  5. package/.next/standalone/.next/prerender-manifest.json +3 -3
  6. package/.next/standalone/.next/required-server-files.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  10. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  14. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  15. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  16. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  17. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  23. package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
  24. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
  25. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  26. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
  27. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  29. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +16 -16
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
  36. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  37. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
  38. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  39. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  40. package/.next/standalone/.next/server/app/page.js +1 -1
  41. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  44. package/.next/standalone/.next/server/app/policies/page.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  52. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  56. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  57. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  58. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  60. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.~nmr9._.js +3 -0
  61. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0yspgjy._.js → [root-of-the-server]__010i6f5._.js} +2 -2
  62. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08px0ym._.js +3 -0
  63. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +3 -0
  64. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +3 -0
  65. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kjo7d_._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wu7fr7._.js +3 -0
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z4c5dj._.js +3 -0
  70. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zso~62._.js +3 -0
  71. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +4 -0
  73. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +4 -0
  74. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09icjsf._.js → [root-of-the-server]__0709m8.._.js} +3 -3
  75. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +4 -0
  76. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +4 -0
  77. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +4 -0
  78. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
  84. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vrlxf2._.js → [root-of-the-server]__0ymn496._.js} +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0eu4j_n._.js → [root-of-the-server]__10h.ggz._.js} +2 -2
  86. package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/ssr/{_07a1g.3._.js → _0zx~s__._.js} +2 -2
  88. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  89. package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  91. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
  92. package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +1 -1
  93. package/.next/standalone/.next/server/chunks/ssr/lib_copilot-projects_ts_0r8xkn8._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/lib_cursor-projects_ts_0qt1scg._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +3 -0
  96. package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/lib_pi-projects_ts_103tsh1._.js +3 -0
  98. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  99. package/.next/standalone/.next/server/pages/404.html +2 -2
  100. package/.next/standalone/.next/server/pages/500.html +1 -1
  101. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  102. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  103. package/.next/standalone/.next/static/chunks/{06j6c0ofqjy0v.js → 00ay03h8bq4b~.js} +2 -2
  104. package/.next/standalone/.next/static/chunks/{15_mi91qaeieu.js → 0agmlhk5ml7x5.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
  106. package/.next/standalone/.next/static/chunks/0en4v5k2nnxks.js +1 -0
  107. package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
  108. package/.next/standalone/.next/static/chunks/{0zdn~84f58hvf.js → 0s6nux54y~l~r.js} +1 -1
  109. package/.next/standalone/.next/static/chunks/{10mlwc4y_kqo2.js → 0tpse0wu2wwo0.js} +1 -1
  110. package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
  111. package/.next/standalone/.next/static/chunks/{0e8c_1f7-8e7t.js → 1400rtd5ywbt..js} +2 -2
  112. package/.next/standalone/.next/static/chunks/{0mumk7h5i.1xd.js → 14lmf8boay-zu.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/{18a9xv2p3~x.9.js → 17htukxga7bil.js} +1 -1
  114. package/.next/standalone/.opencode/opencode.json +4 -0
  115. package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
  116. package/.next/standalone/.pi/settings.json +5 -0
  117. package/.next/standalone/app/components/cli-badge.tsx +7 -11
  118. package/.next/standalone/app/components/project-list.tsx +32 -4
  119. package/.next/standalone/app/policies/hooks-client.tsx +31 -15
  120. package/.next/standalone/app/project/[name]/page.tsx +52 -16
  121. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
  122. package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
  123. package/.next/standalone/assets/logos/copilot-light.svg +1 -0
  124. package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
  125. package/.next/standalone/assets/logos/cursor-light.svg +1 -0
  126. package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
  127. package/.next/standalone/assets/logos/gemini-light.svg +13 -0
  128. package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
  129. package/.next/standalone/assets/logos/opencode-light.svg +1 -0
  130. package/.next/standalone/assets/logos/pi-dark.svg +7 -0
  131. package/.next/standalone/assets/logos/pi-light.svg +7 -0
  132. package/.next/standalone/lib/cli-registry.ts +107 -0
  133. package/.next/standalone/lib/codex-projects.ts +3 -3
  134. package/.next/standalone/lib/copilot-projects.ts +224 -0
  135. package/.next/standalone/lib/copilot-sessions.ts +395 -0
  136. package/.next/standalone/lib/cursor-projects.ts +312 -0
  137. package/.next/standalone/lib/cursor-sessions.ts +467 -0
  138. package/.next/standalone/lib/gemini-projects.ts +203 -0
  139. package/.next/standalone/lib/gemini-sessions.ts +365 -0
  140. package/.next/standalone/lib/opencode-projects.ts +232 -0
  141. package/.next/standalone/lib/opencode-sessions.ts +237 -0
  142. package/.next/standalone/lib/pi-projects.ts +230 -0
  143. package/.next/standalone/lib/pi-sessions.ts +325 -0
  144. package/.next/standalone/lib/projects.ts +67 -31
  145. package/.next/standalone/next.config.ts +5 -4
  146. package/.next/standalone/package.json +2 -1
  147. package/.next/standalone/pi-extension/index.ts +373 -0
  148. package/.next/standalone/pi-extension/package.json +12 -0
  149. package/.next/standalone/server.js +1 -1
  150. package/README.md +37 -3
  151. package/bin/failproofai.mjs +61 -21
  152. package/dist/cli.mjs +2248 -246
  153. package/lib/cli-registry.ts +107 -0
  154. package/lib/codex-projects.ts +3 -3
  155. package/lib/copilot-projects.ts +224 -0
  156. package/lib/copilot-sessions.ts +395 -0
  157. package/lib/cursor-projects.ts +312 -0
  158. package/lib/cursor-sessions.ts +467 -0
  159. package/lib/gemini-projects.ts +203 -0
  160. package/lib/gemini-sessions.ts +365 -0
  161. package/lib/opencode-projects.ts +232 -0
  162. package/lib/opencode-sessions.ts +237 -0
  163. package/lib/pi-projects.ts +230 -0
  164. package/lib/pi-sessions.ts +325 -0
  165. package/lib/projects.ts +67 -31
  166. package/package.json +2 -1
  167. package/pi-extension/index.ts +373 -0
  168. package/pi-extension/package.json +12 -0
  169. package/scripts/translate-docs/mdx-translator.ts +56 -2
  170. package/scripts/translate-docs/translator.ts +1 -1
  171. package/src/hooks/builtin-policies.ts +84 -14
  172. package/src/hooks/handler.ts +67 -5
  173. package/src/hooks/install-prompt.ts +33 -10
  174. package/src/hooks/integrations.ts +1007 -6
  175. package/src/hooks/policy-evaluator.ts +299 -3
  176. package/src/hooks/resolve-permission-mode.ts +23 -0
  177. package/src/hooks/types.ts +307 -3
  178. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
  179. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
  180. package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
  181. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
  182. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
  183. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
  184. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
  191. package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
  192. package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
  193. package/.next/standalone/.next/static/chunks/03egp37o1l629.js +0 -1
  194. package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
  195. package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
  196. /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 68TLSFdjAQYIulNHfP0QY}/_buildManifest.js +0 -0
  197. /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 68TLSFdjAQYIulNHfP0QY}/_clientMiddlewareManifest.js +0 -0
  198. /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 68TLSFdjAQYIulNHfP0QY}/_ssgManifest.js +0 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Cursor Agent CLI project discovery.
3
+ *
4
+ * Cursor stores per-session state under `~/.cursor/` (configurable via
5
+ * CURSOR_HOME). The exact subdirectory layout is not yet documented in
6
+ * cursor.com/docs/hooks; we probe the candidates Cursor has historically
7
+ * shipped (`agent-sessions/`, `conversations/`, `sessions/`) so this works
8
+ * across installs without a hard-coded version pin. Every candidate is a
9
+ * tolerant `safeReaddir` — a missing directory yields `[]`, never an
10
+ * exception.
11
+ *
12
+ * Each session directory is expected to contain at minimum a metadata file
13
+ * carrying the session's `cwd`. When a JSONL transcript is also present we
14
+ * surface the session under `/projects` keyed by encoded cwd so multiple
15
+ * stores naturally merge in `lib/projects.ts`.
16
+ *
17
+ * Refs: https://cursor.com/docs/hooks (env: CURSOR_PROJECT_DIR,
18
+ * CURSOR_TRANSCRIPT_PATH; transcript format intentionally unspecified).
19
+ */
20
+ import { readdir, readFile, stat } from "node:fs/promises";
21
+ import { homedir } from "node:os";
22
+ import { join } from "node:path";
23
+ import { decodeFolderName, encodeFolderName } from "./paths";
24
+ import type { ProjectFolder, SessionFile } from "./projects";
25
+ import { runtimeCache } from "./runtime-cache";
26
+ import { batchAll } from "./concurrency";
27
+ import { formatDate } from "./format-date";
28
+ import { logWarn } from "./logger";
29
+
30
+ /** Legacy subdirectories under `~/.cursor/` that may carry per-session state
31
+ * (cursor-agent ≤ 2026-04 ish — kept as a fallback). */
32
+ const LEGACY_SESSION_ROOT_CANDIDATES = ["agent-sessions", "conversations", "sessions"] as const;
33
+
34
+ /** Legacy filenames that may carry session-level metadata (cwd, model, …). */
35
+ const META_FILE_CANDIDATES = ["meta.json", "session.json", "workspace.json", "workspace.yaml"] as const;
36
+
37
+ /** Legacy filenames that may carry the JSONL transcript. */
38
+ const LEGACY_TRANSCRIPT_FILE_CANDIDATES = ["events.jsonl", "transcript.jsonl", "messages.jsonl"] as const;
39
+
40
+ /** New (2026-04+) layout: `~/.cursor/projects/<encoded-cwd>/agent-transcripts/<sessionId>/<sessionId>.jsonl`. */
41
+ const NEW_PROJECTS_DIR = "projects";
42
+ const NEW_AGENT_TRANSCRIPTS_DIR = "agent-transcripts";
43
+
44
+ function getCursorHome(): string {
45
+ return process.env.CURSOR_HOME || join(homedir(), ".cursor");
46
+ }
47
+
48
+ interface CursorSessionMeta {
49
+ metaPath: string;
50
+ transcriptPath: string | null;
51
+ sessionId: string;
52
+ cwd: string;
53
+ fileMtime: Date;
54
+ hasTranscript: boolean;
55
+ }
56
+
57
+ async function safeReaddir(dir: string) {
58
+ try {
59
+ return await readdir(dir, { withFileTypes: true });
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ async function statMtime(path: string): Promise<Date | null> {
66
+ try {
67
+ return (await stat(path)).mtime;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /** Parse a flat scalar for `cwd` from JSON-or-YAML metadata. Tries `JSON.parse`
74
+ * first so escape sequences in JSON strings (e.g. Windows `C:\\Users\\...`) are
75
+ * decoded, then falls back to a permissive YAML-ish regex when the file isn't
76
+ * valid JSON. Tolerant on purpose: Cursor's metadata format is unspecified. */
77
+ function parseCwdFromMetaText(text: string): string | undefined {
78
+ try {
79
+ const parsed = JSON.parse(text) as { cwd?: unknown };
80
+ if (typeof parsed.cwd === "string" && parsed.cwd.length > 0) return parsed.cwd;
81
+ } catch {
82
+ // Not JSON — fall through to the YAML-ish parser.
83
+ }
84
+ // YAML shape: `cwd: /path` (optionally quoted).
85
+ const yaml = text.match(/^\s*cwd\s*:\s*(.+?)\s*$/m);
86
+ if (yaml) {
87
+ const stripped = yaml[1].replace(/^['"]|['"]$/g, "");
88
+ if (stripped.length > 0) return stripped;
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ /** Try each candidate under `dir`; for metadata files we keep probing until one
94
+ * yields a usable `cwd` (a stale `meta.json` shouldn't shadow a valid
95
+ * `workspace.yaml`). Returns `{path, cwd}` for the first usable file, or null. */
96
+ async function findFirstUsableMeta(
97
+ dir: string,
98
+ candidates: readonly string[],
99
+ ): Promise<{ path: string; cwd: string } | null> {
100
+ for (const name of candidates) {
101
+ const path = join(dir, name);
102
+ if ((await statMtime(path)) === null) continue;
103
+ let text: string;
104
+ try {
105
+ text = await readFile(path, "utf-8");
106
+ } catch {
107
+ continue;
108
+ }
109
+ const cwd = parseCwdFromMetaText(text);
110
+ if (cwd) return { path, cwd };
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /** First existing path from `candidates` under `dir`. Used for transcript files
116
+ * where existence is the only check we can perform without parsing JSONL. */
117
+ async function findFirstExistingPath(dir: string, candidates: readonly string[]): Promise<string | null> {
118
+ for (const name of candidates) {
119
+ const path = join(dir, name);
120
+ if ((await statMtime(path)) !== null) return path;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ async function scanCursorSessions(): Promise<CursorSessionMeta[]> {
126
+ const home = getCursorHome();
127
+ const newCandidates: { sessionId: string; dir: string; cwd: string }[] = [];
128
+ const legacyCandidates: { sessionId: string; dir: string }[] = [];
129
+
130
+ // New layout: ~/.cursor/projects/<encoded-cwd>/agent-transcripts/<sessionId>/<sessionId>.jsonl
131
+ // Cursor's project-folder encoding drops the leading slash (e.g.
132
+ // `home-u-repo` decodes to `/home/u/repo`, not `home/u/repo`). Re-add the
133
+ // leading slash for non-Windows-drive-letter results so the cwd is absolute.
134
+ const projectsRoot = join(home, NEW_PROJECTS_DIR);
135
+ const projectEntries = await safeReaddir(projectsRoot);
136
+ if (projectEntries) {
137
+ for (const proj of projectEntries) {
138
+ if (!proj.isDirectory()) continue;
139
+ const decoded = decodeFolderName(proj.name);
140
+ const cwd = decoded.startsWith("/") || /^[A-Za-z]:\//.test(decoded) ? decoded : `/${decoded}`;
141
+ const transcriptsRoot = join(projectsRoot, proj.name, NEW_AGENT_TRANSCRIPTS_DIR);
142
+ const sessionDirs = await safeReaddir(transcriptsRoot);
143
+ if (!sessionDirs) continue;
144
+ for (const sd of sessionDirs) {
145
+ if (!sd.isDirectory()) continue;
146
+ newCandidates.push({
147
+ sessionId: sd.name,
148
+ dir: join(transcriptsRoot, sd.name),
149
+ cwd,
150
+ });
151
+ }
152
+ }
153
+ }
154
+
155
+ // Legacy layout: ~/.cursor/{agent-sessions,conversations,sessions}/<sessionId>/
156
+ for (const sub of LEGACY_SESSION_ROOT_CANDIDATES) {
157
+ const root = join(home, sub);
158
+ const entries = await safeReaddir(root);
159
+ if (!entries) continue;
160
+ for (const e of entries) {
161
+ if (!e.isDirectory()) continue;
162
+ legacyCandidates.push({ sessionId: e.name, dir: join(root, e.name) });
163
+ }
164
+ }
165
+
166
+ if (newCandidates.length === 0 && legacyCandidates.length === 0) return [];
167
+
168
+ const settled = await batchAll(
169
+ [
170
+ ...newCandidates.map((c) => async (): Promise<CursorSessionMeta | null> => {
171
+ // Transcript file is `<sessionId>.jsonl` inside the dir.
172
+ const transcriptPath = join(c.dir, `${c.sessionId}.jsonl`);
173
+ const transcriptMtime = await statMtime(transcriptPath);
174
+ if (!transcriptMtime) return null;
175
+ return {
176
+ metaPath: c.dir, // No separate meta file; cwd is decoded from the parent dir name.
177
+ transcriptPath,
178
+ sessionId: c.sessionId,
179
+ cwd: c.cwd,
180
+ fileMtime: transcriptMtime,
181
+ hasTranscript: true,
182
+ };
183
+ }),
184
+ ...legacyCandidates.map((c) => async (): Promise<CursorSessionMeta | null> => {
185
+ const meta = await findFirstUsableMeta(c.dir, META_FILE_CANDIDATES);
186
+ if (!meta) return null;
187
+ const transcriptPath = await findFirstExistingPath(c.dir, LEGACY_TRANSCRIPT_FILE_CANDIDATES);
188
+ const transcriptMtime = transcriptPath ? await statMtime(transcriptPath) : null;
189
+ const metaMtime = await statMtime(meta.path);
190
+ const fileMtime =
191
+ transcriptMtime && metaMtime
192
+ ? new Date(Math.max(transcriptMtime.getTime(), metaMtime.getTime()))
193
+ : transcriptMtime ?? metaMtime ?? new Date(0);
194
+ return {
195
+ metaPath: meta.path,
196
+ transcriptPath,
197
+ sessionId: c.sessionId,
198
+ cwd: meta.cwd,
199
+ fileMtime,
200
+ hasTranscript: transcriptPath !== null,
201
+ };
202
+ }),
203
+ ],
204
+ 16,
205
+ );
206
+ return settled
207
+ .filter((r): r is PromiseFulfilledResult<CursorSessionMeta | null> => r.status === "fulfilled")
208
+ .map((r) => r.value)
209
+ .filter((v): v is CursorSessionMeta => v !== null);
210
+ }
211
+
212
+ const cachedScan = runtimeCache(scanCursorSessions, 30);
213
+
214
+ /** Returns one ProjectFolder per unique cwd discovered in Cursor transcripts. */
215
+ export async function getCursorProjects(): Promise<ProjectFolder[]> {
216
+ let metas: CursorSessionMeta[];
217
+ try {
218
+ metas = await cachedScan();
219
+ } catch (error) {
220
+ logWarn("Failed to scan Cursor sessions:", error);
221
+ return [];
222
+ }
223
+
224
+ // Skip metadata-only sessions whose `/projects` row would click through to an
225
+ // empty session list (metasToSessionFiles also filters on hasTranscript).
226
+ const byCwd = new Map<string, { latest: Date; cwd: string }>();
227
+ for (const m of metas) {
228
+ if (!m.hasTranscript) continue;
229
+ const existing = byCwd.get(m.cwd);
230
+ if (!existing || m.fileMtime.getTime() > existing.latest.getTime()) {
231
+ byCwd.set(m.cwd, { latest: m.fileMtime, cwd: m.cwd });
232
+ }
233
+ }
234
+
235
+ const folders: ProjectFolder[] = [];
236
+ for (const { cwd, latest } of byCwd.values()) {
237
+ folders.push({
238
+ name: encodeFolderName(cwd),
239
+ path: cwd,
240
+ isDirectory: true,
241
+ lastModified: latest,
242
+ lastModifiedFormatted: formatDate(latest),
243
+ cli: ["cursor"],
244
+ });
245
+ }
246
+ folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
247
+ return folders;
248
+ }
249
+
250
+ function metasToSessionFiles(metas: CursorSessionMeta[]): SessionFile[] {
251
+ const files: SessionFile[] = metas
252
+ .filter((m) => m.hasTranscript && m.transcriptPath)
253
+ .map((m) => ({
254
+ name: m.sessionId,
255
+ path: m.transcriptPath!,
256
+ lastModified: m.fileMtime,
257
+ lastModifiedFormatted: formatDate(m.fileMtime),
258
+ sessionId: m.sessionId,
259
+ cli: "cursor",
260
+ }));
261
+ files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
262
+ return files;
263
+ }
264
+
265
+ /** Returns SessionFile entries for every Cursor transcript whose cwd matches `cwd` exactly. */
266
+ export async function getCursorSessionsForCwd(cwd: string): Promise<SessionFile[]> {
267
+ let metas: CursorSessionMeta[];
268
+ try {
269
+ metas = await cachedScan();
270
+ } catch (error) {
271
+ logWarn("Failed to scan Cursor sessions:", error);
272
+ return [];
273
+ }
274
+ return metasToSessionFiles(metas.filter((m) => m.cwd === cwd));
275
+ }
276
+
277
+ export interface CursorProjectByName {
278
+ cwd: string | null;
279
+ sessions: SessionFile[];
280
+ }
281
+
282
+ /**
283
+ * Looks up Cursor sessions for a project URL slug. `decodeFolderName` is lossy,
284
+ * so we re-encode each session's cwd and match in that direction. Returns both
285
+ * the canonical cwd and the matching sessions.
286
+ */
287
+ export async function getCursorSessionsByEncodedName(name: string): Promise<CursorProjectByName> {
288
+ let metas: CursorSessionMeta[];
289
+ try {
290
+ metas = await cachedScan();
291
+ } catch (error) {
292
+ logWarn("Failed to scan Cursor sessions:", error);
293
+ return { cwd: null, sessions: [] };
294
+ }
295
+ const matches = metas.filter((m) => m.hasTranscript && encodeFolderName(m.cwd) === name);
296
+ return {
297
+ cwd: matches[0]?.cwd ?? null,
298
+ sessions: metasToSessionFiles(matches),
299
+ };
300
+ }
301
+
302
+ export const getCachedCursorProjects = runtimeCache(getCursorProjects, 30);
303
+ export const getCachedCursorSessionsForCwd = runtimeCache(
304
+ (cwd: string) => getCursorSessionsForCwd(cwd),
305
+ 30,
306
+ { maxSize: 50 },
307
+ );
308
+ export const getCachedCursorSessionsByEncodedName = runtimeCache(
309
+ (name: string) => getCursorSessionsByEncodedName(name),
310
+ 30,
311
+ { maxSize: 50 },
312
+ );