failproofai 0.0.9 → 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 (197) 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]__0_b7pgn._.js → [root-of-the-server]__0ymn496._.js} +2 -2
  85. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.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/{0n-_j_6fo6jex.js → 00ay03h8bq4b~.js} +2 -2
  104. package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.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/{095l4hc7-h.~~.js → 0en4v5k2nnxks.js} +1 -1
  107. package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
  108. package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0s6nux54y~l~r.js} +1 -1
  109. package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.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/{0u-ys71jc4y68.js → 1400rtd5ywbt..js} +2 -2
  112. package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 14lmf8boay-zu.js} +1 -1
  113. package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.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/06x4-d1~o-opr.js +0 -1
  194. package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
  195. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_buildManifest.js +0 -0
  196. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_clientMiddlewareManifest.js +0 -0
  197. /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_ssgManifest.js +0 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Single source of truth for agent-CLI metadata used by the dashboard, the
3
+ * `bin/failproofai.mjs` argv parser, the install prompt, and the badge UI.
4
+ *
5
+ * This module is **client-safe** — it only exports plain string metadata. The
6
+ * server-side project / session providers live in their own files
7
+ * (`lib/codex-projects.ts`, `lib/codex-sessions.ts`, `lib/copilot-projects.ts`,
8
+ * `lib/copilot-sessions.ts`, `lib/cursor-projects.ts`, `lib/cursor-sessions.ts`,
9
+ * `lib/opencode-projects.ts`, `lib/opencode-sessions.ts`,
10
+ * `lib/pi-projects.ts`, `lib/pi-sessions.ts`)
11
+ * and are imported lazily by `lib/projects.ts` and the session viewer page so
12
+ * Turbopack doesn't drag Node-only deps (`fs/promises`, `os`) into client
13
+ * bundles.
14
+ *
15
+ * Adding a new agent CLI = three steps:
16
+ * 1. Extend `INTEGRATION_TYPES` in `src/hooks/types.ts` (server-side hook contract).
17
+ * 2. Add an `Integration` impl in `src/hooks/integrations.ts` (install/uninstall plumbing).
18
+ * 3. Add an entry to `CLI_REGISTRY` below — display label and badge classes.
19
+ * Optionally add a project provider (`lib/<cli>-projects.ts`) and a
20
+ * session loader (`lib/<cli>-sessions.ts`); wire them into
21
+ * `lib/projects.ts#getProjectFolders` and the session viewer page's
22
+ * fallback chain (both already iterate over per-CLI providers).
23
+ *
24
+ * Filter dropdown, badge component, and project-list filter all read from
25
+ * this registry and pick up new CLIs without further code changes.
26
+ */
27
+ import type { IntegrationType } from "@/src/hooks/types";
28
+
29
+ /** Canonical CLI ids the registry knows about. Mirrors `INTEGRATION_TYPES`. */
30
+ export const KNOWN_CLI_IDS = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"] as const satisfies readonly IntegrationType[];
31
+ export type CliId = (typeof KNOWN_CLI_IDS)[number];
32
+
33
+ /** Per-CLI metadata consumed by the dashboard. */
34
+ export interface CliEntry {
35
+ id: CliId;
36
+ label: string;
37
+ /** Tailwind utility classes for the small CLI badge (background + text + border). */
38
+ badgeClasses: string;
39
+ }
40
+
41
+ const CLI_ENTRIES: Record<CliId, CliEntry> = {
42
+ claude: {
43
+ id: "claude",
44
+ label: "Claude Code",
45
+ badgeClasses: "bg-orange-500/10 text-orange-400 border-orange-500/20",
46
+ },
47
+ codex: {
48
+ id: "codex",
49
+ label: "OpenAI Codex",
50
+ badgeClasses: "bg-purple-500/10 text-purple-400 border-purple-500/20",
51
+ },
52
+ copilot: {
53
+ id: "copilot",
54
+ label: "GitHub Copilot",
55
+ badgeClasses: "bg-blue-500/10 text-blue-400 border-blue-500/20",
56
+ },
57
+ cursor: {
58
+ id: "cursor",
59
+ label: "Cursor Agent",
60
+ badgeClasses: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
61
+ },
62
+ opencode: {
63
+ id: "opencode",
64
+ label: "OpenCode",
65
+ badgeClasses: "bg-amber-500/10 text-amber-400 border-amber-500/20",
66
+ },
67
+ pi: {
68
+ id: "pi",
69
+ label: "Pi",
70
+ badgeClasses: "bg-pink-500/10 text-pink-400 border-pink-500/20",
71
+ },
72
+ gemini: {
73
+ id: "gemini",
74
+ label: "Gemini CLI",
75
+ badgeClasses: "bg-sky-500/10 text-sky-400 border-sky-500/20",
76
+ },
77
+ };
78
+
79
+ export function getCliEntry(id: string): CliEntry | undefined {
80
+ return CLI_ENTRIES[id as CliId];
81
+ }
82
+
83
+ export function listCliEntries(): CliEntry[] {
84
+ return KNOWN_CLI_IDS.map((id) => CLI_ENTRIES[id]);
85
+ }
86
+
87
+ /** External CLIs (everything except Claude) that contribute project listings. */
88
+ export function listExternalCliEntries(): CliEntry[] {
89
+ return listCliEntries().filter((c) => c.id !== "claude");
90
+ }
91
+
92
+ /** Display label for a CLI id. Returns the id itself if unknown. */
93
+ export function getCliLabel(id: string): string {
94
+ return getCliEntry(id)?.label ?? id;
95
+ }
96
+
97
+ /** Badge classes for a CLI id. Falls back to Claude's classes if unknown. */
98
+ export function getCliBadgeClasses(id: string): string {
99
+ return getCliEntry(id)?.badgeClasses ?? CLI_ENTRIES.claude.badgeClasses;
100
+ }
101
+
102
+ /** Predicate: is this id a known CLI? Useful when validating user input. */
103
+ export function isKnownCli(id: string | null | undefined): id is CliId {
104
+ // hasOwnProperty.call (not `id in CLI_ENTRIES`) so inherited Object.prototype
105
+ // keys like "toString" / "constructor" / "hasOwnProperty" don't pass.
106
+ return typeof id === "string" && Object.prototype.hasOwnProperty.call(CLI_ENTRIES, id);
107
+ }
@@ -9,9 +9,9 @@
9
9
  * convention (see `encodeFolderName` in `lib/paths.ts`), so a cwd present in both stores
10
10
  * naturally produces the same `name` and can be merged on the Claude side.
11
11
  */
12
- import { open, readdir } from "fs/promises";
13
- import { homedir } from "os";
14
- import { join } from "path";
12
+ import { open, readdir } from "node:fs/promises";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
15
  import { encodeFolderName } from "./paths";
16
16
  import type { ProjectFolder, SessionFile } from "./projects";
17
17
  import { runtimeCache } from "./runtime-cache";
@@ -0,0 +1,224 @@
1
+ /**
2
+ * GitHub Copilot CLI project discovery.
3
+ *
4
+ * Copilot stores per-session state at `~/.copilot/session-state/<sessionId>/`,
5
+ * with a `workspace.yaml` carrying flat scalars: id, cwd, git_root, branch,
6
+ * repository, host_type, user_named, summary_count, created_at, updated_at,
7
+ * (optional) name, summary. We read this file (always present, even before
8
+ * any interaction creates events.jsonl) to extract the cwd. Sessions are
9
+ * grouped by unique cwd into `ProjectFolder` rows.
10
+ *
11
+ * The encoded cwd doubles as the URL slug for `/project/[name]`, matching the
12
+ * Claude Code convention (see `encodeFolderName` in `lib/paths.ts`), so a cwd
13
+ * present in multiple stores naturally produces the same `name` and merges in
14
+ * `lib/projects.ts`.
15
+ */
16
+ import { readdir, readFile, stat } from "node:fs/promises";
17
+ import { homedir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { encodeFolderName } from "./paths";
20
+ import type { ProjectFolder, SessionFile } from "./projects";
21
+ import { runtimeCache } from "./runtime-cache";
22
+ import { batchAll } from "./concurrency";
23
+ import { formatDate } from "./format-date";
24
+ import { logWarn } from "./logger";
25
+
26
+ /** Inlined to avoid cross-module imports from `lib/copilot-sessions.ts` —
27
+ * keeping the dep tree independent prevents Turbopack from tracing
28
+ * Node-only modules (`fs/promises`, `os`) into the client bundle when the
29
+ * session viewer page statically imports `copilot-sessions`. Mirrors the
30
+ * pattern in `lib/codex-projects.ts`. */
31
+ function getCopilotSessionStateRoot(): string {
32
+ return join(process.env.COPILOT_HOME || join(homedir(), ".copilot"), "session-state");
33
+ }
34
+
35
+ interface CopilotSessionMeta {
36
+ workspacePath: string;
37
+ eventsPath: string;
38
+ sessionId: string;
39
+ cwd: string;
40
+ /** Latest of (workspace.yaml mtime, events.jsonl mtime if present). */
41
+ fileMtime: Date;
42
+ /** True iff `events.jsonl` exists. Workspace-only sessions (initialized but
43
+ * never sent a prompt) skip the `/project` session list because the viewer
44
+ * would only render "Session log file not found." */
45
+ hasTranscript: boolean;
46
+ }
47
+
48
+ async function safeReaddir(dir: string) {
49
+ try {
50
+ return await readdir(dir, { withFileTypes: true });
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ /** Extract `cwd` from a workspace.yaml file. Permissive regex parser — avoids
57
+ * pulling in a YAML lib for one flat scalar. Copilot writes simple
58
+ * `key: value` lines without nesting in this file (verified against CLI 1.0.39). */
59
+ function parseCwdFromWorkspace(text: string): string | undefined {
60
+ const m = text.match(/^cwd\s*:\s*(.+?)\s*$/m);
61
+ if (!m) return undefined;
62
+ return m[1].replace(/^['"]|['"]$/g, "");
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
+ async function scanCopilotSessions(): Promise<CopilotSessionMeta[]> {
74
+ const root = getCopilotSessionStateRoot();
75
+ const entries = await safeReaddir(root);
76
+ if (!entries) return [];
77
+
78
+ const candidates = entries
79
+ .filter((e) => e.isDirectory())
80
+ .map((e) => ({
81
+ sessionId: e.name,
82
+ workspacePath: join(root, e.name, "workspace.yaml"),
83
+ eventsPath: join(root, e.name, "events.jsonl"),
84
+ }));
85
+
86
+ const settled = await batchAll(
87
+ candidates.map((c) => async (): Promise<CopilotSessionMeta | null> => {
88
+ let workspaceText: string;
89
+ try {
90
+ workspaceText = await readFile(c.workspacePath, "utf-8");
91
+ } catch {
92
+ return null;
93
+ }
94
+ const cwd = parseCwdFromWorkspace(workspaceText);
95
+ if (!cwd) return null;
96
+ // Prefer events.jsonl mtime when present (reflects last activity);
97
+ // fall back to workspace.yaml mtime for sessions without interaction.
98
+ const eventsMtime = await statMtime(c.eventsPath);
99
+ const wsMtime = await statMtime(c.workspacePath);
100
+ const hasTranscript = eventsMtime !== null;
101
+ const fileMtime =
102
+ eventsMtime && wsMtime
103
+ ? new Date(Math.max(eventsMtime.getTime(), wsMtime.getTime()))
104
+ : eventsMtime ?? wsMtime ?? new Date(0);
105
+ return {
106
+ workspacePath: c.workspacePath,
107
+ eventsPath: c.eventsPath,
108
+ sessionId: c.sessionId,
109
+ cwd,
110
+ fileMtime,
111
+ hasTranscript,
112
+ };
113
+ }),
114
+ 16,
115
+ );
116
+ return settled
117
+ .filter((r): r is PromiseFulfilledResult<CopilotSessionMeta | null> => r.status === "fulfilled")
118
+ .map((r) => r.value)
119
+ .filter((v): v is CopilotSessionMeta => v !== null);
120
+ }
121
+
122
+ const cachedScan = runtimeCache(scanCopilotSessions, 30);
123
+
124
+ /** Returns one ProjectFolder per unique cwd discovered in Copilot transcripts. */
125
+ export async function getCopilotProjects(): Promise<ProjectFolder[]> {
126
+ let metas: CopilotSessionMeta[];
127
+ try {
128
+ metas = await cachedScan();
129
+ } catch (error) {
130
+ logWarn("Failed to scan Copilot sessions:", error);
131
+ return [];
132
+ }
133
+
134
+ // Skip workspace-only sessions: their /projects rows would click through to
135
+ // an empty session list (metasToSessionFiles also filters on hasTranscript).
136
+ const byCwd = new Map<string, { latest: Date; cwd: string }>();
137
+ for (const m of metas) {
138
+ if (!m.hasTranscript) continue;
139
+ const existing = byCwd.get(m.cwd);
140
+ if (!existing || m.fileMtime.getTime() > existing.latest.getTime()) {
141
+ byCwd.set(m.cwd, { latest: m.fileMtime, cwd: m.cwd });
142
+ }
143
+ }
144
+
145
+ const folders: ProjectFolder[] = [];
146
+ for (const { cwd, latest } of byCwd.values()) {
147
+ folders.push({
148
+ name: encodeFolderName(cwd),
149
+ path: cwd,
150
+ isDirectory: true,
151
+ lastModified: latest,
152
+ lastModifiedFormatted: formatDate(latest),
153
+ cli: ["copilot"],
154
+ });
155
+ }
156
+ folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
157
+ return folders;
158
+ }
159
+
160
+ function metasToSessionFiles(metas: CopilotSessionMeta[]): SessionFile[] {
161
+ // Skip workspace-only sessions: their "openable" rows would lead to a
162
+ // 'Session log file not found' viewer because events.jsonl doesn't exist.
163
+ const files: SessionFile[] = metas
164
+ .filter((m) => m.hasTranscript)
165
+ .map((m) => ({
166
+ name: m.sessionId,
167
+ path: m.eventsPath,
168
+ lastModified: m.fileMtime,
169
+ lastModifiedFormatted: formatDate(m.fileMtime),
170
+ sessionId: m.sessionId,
171
+ cli: "copilot",
172
+ }));
173
+ files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
174
+ return files;
175
+ }
176
+
177
+ /** Returns SessionFile entries for every Copilot transcript whose cwd matches `cwd` exactly. */
178
+ export async function getCopilotSessionsForCwd(cwd: string): Promise<SessionFile[]> {
179
+ let metas: CopilotSessionMeta[];
180
+ try {
181
+ metas = await cachedScan();
182
+ } catch (error) {
183
+ logWarn("Failed to scan Copilot sessions:", error);
184
+ return [];
185
+ }
186
+ return metasToSessionFiles(metas.filter((m) => m.cwd === cwd));
187
+ }
188
+
189
+ export interface CopilotProjectByName {
190
+ cwd: string | null;
191
+ sessions: SessionFile[];
192
+ }
193
+
194
+ /**
195
+ * Looks up Copilot sessions for a project URL slug. `decodeFolderName` is lossy
196
+ * (every `-` becomes `/`), so we re-encode each session's cwd and match in
197
+ * that direction. Returns both the canonical cwd and the matching sessions.
198
+ */
199
+ export async function getCopilotSessionsByEncodedName(name: string): Promise<CopilotProjectByName> {
200
+ let metas: CopilotSessionMeta[];
201
+ try {
202
+ metas = await cachedScan();
203
+ } catch (error) {
204
+ logWarn("Failed to scan Copilot sessions:", error);
205
+ return { cwd: null, sessions: [] };
206
+ }
207
+ const matches = metas.filter((m) => m.hasTranscript && encodeFolderName(m.cwd) === name);
208
+ return {
209
+ cwd: matches[0]?.cwd ?? null,
210
+ sessions: metasToSessionFiles(matches),
211
+ };
212
+ }
213
+
214
+ export const getCachedCopilotProjects = runtimeCache(getCopilotProjects, 30);
215
+ export const getCachedCopilotSessionsForCwd = runtimeCache(
216
+ (cwd: string) => getCopilotSessionsForCwd(cwd),
217
+ 30,
218
+ { maxSize: 50 },
219
+ );
220
+ export const getCachedCopilotSessionsByEncodedName = runtimeCache(
221
+ (name: string) => getCopilotSessionsByEncodedName(name),
222
+ 30,
223
+ { maxSize: 50 },
224
+ );