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,325 @@
1
+ /**
2
+ * Pi (pi-coding-agent) session transcript discovery + JSONL parser.
3
+ *
4
+ * Empirically verified against pi-coding-agent v0.72.1 (Phase 0.7 of plan):
5
+ *
6
+ * Session files live at
7
+ * `~/.pi/agent/sessions/<encoded-cwd>/<ISO-timestamp>_<UUID>.jsonl`
8
+ * where `<encoded-cwd>` wraps `--`-prefixed-and-suffixed `/`-separated paths
9
+ * (e.g. `/home/user/repo` → `--home-user-repo--`). The encoding is lossy
10
+ * (literal `-` is preserved); we use the `cwd` field of the first JSONL
11
+ * record (`{type: "session", cwd, …}`) as the canonical cwd.
12
+ *
13
+ * Record schema (observed):
14
+ * {type: "session", version, id, timestamp, cwd}
15
+ * {type: "model_change", id, parentId, timestamp, provider, modelId}
16
+ * {type: "thinking_level_change", id, parentId, timestamp, thinkingLevel}
17
+ * {type: "message", id, parentId, timestamp,
18
+ * message: {role, content[], timestamp}}
19
+ *
20
+ * `message.content[]` items can be `{type: "text", text}` or
21
+ * `{type: "thinking", thinking, thinkingSignature}`. Tool-call blocks are not
22
+ * yet observed in this codebase (no tool-using runs were captured during
23
+ * Phase 0); when Pi does emit them, this parser preserves them as-is via the
24
+ * fallback "system" branch and the test suite asserts at least the
25
+ * round-trip rather than a specific shape.
26
+ */
27
+ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
28
+ import { readFile } from "node:fs/promises";
29
+ import { join, resolve, sep } from "node:path";
30
+ import { homedir } from "node:os";
31
+ import { runtimeCache } from "./runtime-cache";
32
+ import {
33
+ baseEntry,
34
+ formatTimestamp,
35
+ type LogEntry,
36
+ type UserEntry,
37
+ type AssistantEntry,
38
+ type GenericEntry,
39
+ type QueueOperationEntry,
40
+ type ContentBlock,
41
+ type LogSource,
42
+ } from "./log-entries";
43
+
44
+ // ── Paths ──
45
+
46
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
47
+ const SESSION_FILE_RE = /^[\d-]+T[\d-]+Z_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
48
+
49
+ /** Root directory for Pi session state, honoring PI_SESSIONS_DIR. */
50
+ export function getPiSessionStateRoot(): string {
51
+ return process.env.PI_SESSIONS_DIR
52
+ || join(homedir(), ".pi", "agent", "sessions");
53
+ }
54
+
55
+ /** Reject a sessionId that isn't a UUID — defends against path traversal. */
56
+ function isSafeSessionId(sessionId: string): boolean {
57
+ return UUID_RE.test(sessionId);
58
+ }
59
+
60
+ /** Find the JSONL transcript for `sessionId` by walking each per-cwd subdir
61
+ * of the session-state root. Rejects path-traversal sessionIds and verifies
62
+ * the resolved path stays under the root. Returns null on miss. */
63
+ export function findPiTranscript(sessionId: string): string | null {
64
+ if (!isSafeSessionId(sessionId)) return null;
65
+ const root = resolve(getPiSessionStateRoot());
66
+
67
+ let cwdDirs: string[];
68
+ try {
69
+ cwdDirs = readdirSync(root);
70
+ } catch {
71
+ return null;
72
+ }
73
+
74
+ for (const cwdDir of cwdDirs) {
75
+ const cwdPath = resolve(root, cwdDir);
76
+ if (!cwdPath.startsWith(`${root}${sep}`)) continue;
77
+ let files: string[];
78
+ try {
79
+ files = readdirSync(cwdPath);
80
+ } catch {
81
+ continue;
82
+ }
83
+ for (const f of files) {
84
+ const m = SESSION_FILE_RE.exec(f);
85
+ if (!m || m[1].toLowerCase() !== sessionId.toLowerCase()) continue;
86
+ const candidate = resolve(cwdPath, f);
87
+ if (!candidate.startsWith(`${cwdPath}${sep}`)) continue;
88
+ if (existsSync(candidate)) return candidate;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+
94
+ // ── Parser ──
95
+
96
+ interface PiSessionRecord {
97
+ type?: string;
98
+ id?: string;
99
+ parentId?: string | null;
100
+ timestamp?: string;
101
+ cwd?: string;
102
+ version?: number;
103
+ provider?: string;
104
+ modelId?: string;
105
+ thinkingLevel?: string;
106
+ message?: {
107
+ role?: string;
108
+ content?: Array<Record<string, unknown>>;
109
+ timestamp?: number;
110
+ };
111
+ }
112
+
113
+ interface PiParseResult {
114
+ entries: LogEntry[];
115
+ rawLines: Record<string, unknown>[];
116
+ /** Working directory pulled from the first session record, when available. */
117
+ cwd?: string;
118
+ }
119
+
120
+ /** Extract a plain-text summary of a Pi message content block. Concatenates
121
+ * every `"text"` block (joined by blank lines) so multi-part user messages
122
+ * aren't truncated to just the first text segment. */
123
+ function extractMessageText(content: Array<Record<string, unknown>> | undefined): string {
124
+ if (!Array.isArray(content)) return "";
125
+ const parts: string[] = [];
126
+ for (const block of content) {
127
+ if (block?.type === "text" && typeof block.text === "string") parts.push(block.text);
128
+ }
129
+ return parts.join("\n\n");
130
+ }
131
+
132
+ /** Build a list of ContentBlocks for the assistant entry, preserving text and
133
+ * thinking blocks. Skips blocks with non-string payloads (typeof guards). */
134
+ function buildAssistantContent(content: Array<Record<string, unknown>> | undefined): ContentBlock[] {
135
+ if (!Array.isArray(content)) return [];
136
+ const blocks: ContentBlock[] = [];
137
+ for (const block of content) {
138
+ if (block?.type === "text" && typeof block.text === "string" && block.text.length > 0) {
139
+ blocks.push({ type: "text", text: block.text });
140
+ }
141
+ // Pi's "thinking" blocks aren't a first-class entry type in our LogEntry
142
+ // hierarchy; embed as a text block prefixed for clarity.
143
+ if (block?.type === "thinking" && typeof block.thinking === "string" && block.thinking.length > 0) {
144
+ blocks.push({ type: "text", text: `[thinking] ${block.thinking}` });
145
+ }
146
+ }
147
+ return blocks;
148
+ }
149
+
150
+ /**
151
+ * Parse a Pi JSONL transcript into `LogEntry[]` plus the raw lines.
152
+ * Yields to the event loop every 200 lines so big transcripts don't block
153
+ * the request.
154
+ */
155
+ export async function parsePiLog(
156
+ fileContent: string,
157
+ source: LogSource = "session",
158
+ ): Promise<PiParseResult> {
159
+ const lines = fileContent.split("\n").filter((line) => line.trim() !== "");
160
+ const entries: LogEntry[] = [];
161
+ const rawLines: Record<string, unknown>[] = [];
162
+ let cwd: string | undefined;
163
+ let seenSessionStart = false;
164
+
165
+ for (let i = 0; i < lines.length; i++) {
166
+ if (i > 0 && i % 200 === 0) await new Promise<void>((r) => setImmediate(r));
167
+
168
+ const line = lines[i];
169
+ let raw: PiSessionRecord;
170
+ try {
171
+ raw = JSON.parse(line) as PiSessionRecord;
172
+ } catch {
173
+ continue;
174
+ }
175
+
176
+ const rawCopy = { ...(raw as Record<string, unknown>), _source: source };
177
+ rawLines.push(rawCopy);
178
+
179
+ const timestampStr = raw.timestamp;
180
+ if (!timestampStr) continue;
181
+ const date = new Date(timestampStr);
182
+ if (Number.isNaN(date.getTime())) continue;
183
+ const timestamp = date.toISOString();
184
+
185
+ const recType = raw.type;
186
+
187
+ // Pi's first record per session is `{type: "session", cwd, ...}`.
188
+ if (recType === "session") {
189
+ if (typeof raw.cwd === "string" && !cwd) cwd = raw.cwd;
190
+ const label: QueueOperationEntry["label"] = seenSessionStart ? "Session Resumed" : "Session Started";
191
+ seenSessionStart = true;
192
+ entries.push({
193
+ type: "queue-operation",
194
+ ...baseEntry(rawCopy, timestamp, date, source),
195
+ label,
196
+ } satisfies QueueOperationEntry);
197
+ continue;
198
+ }
199
+
200
+ // Pi messages are `{type: "message", message: {role, content[]}}`. Branch
201
+ // on role; render text/thinking content. Validate types defensively.
202
+ if (recType === "message" && raw.message && typeof raw.message === "object") {
203
+ const role = raw.message.role;
204
+ const content = raw.message.content;
205
+
206
+ if (role === "user") {
207
+ const text = extractMessageText(content);
208
+ if (!text) continue;
209
+ entries.push({
210
+ type: "user",
211
+ ...baseEntry(rawCopy, timestamp, date, source),
212
+ message: { role: "user", content: text },
213
+ } satisfies UserEntry);
214
+ continue;
215
+ }
216
+
217
+ if (role === "assistant") {
218
+ const blocks = buildAssistantContent(content);
219
+ if (blocks.length === 0) {
220
+ entries.push({
221
+ type: "system",
222
+ ...baseEntry(rawCopy, timestamp, date, source),
223
+ raw: rawCopy,
224
+ } satisfies GenericEntry);
225
+ continue;
226
+ }
227
+ entries.push({
228
+ type: "assistant",
229
+ ...baseEntry(rawCopy, timestamp, date, source),
230
+ message: { role: "assistant", content: blocks },
231
+ } satisfies AssistantEntry);
232
+ continue;
233
+ }
234
+
235
+ // Unknown role — preserve raw so nothing is dropped.
236
+ entries.push({
237
+ type: "system",
238
+ ...baseEntry(rawCopy, timestamp, date, source),
239
+ raw: rawCopy,
240
+ } satisfies GenericEntry);
241
+ continue;
242
+ }
243
+
244
+ // model_change / thinking_level_change / unknown — preserve raw as system
245
+ // so the dashboard can surface them without ad-hoc renderers.
246
+ entries.push({
247
+ type: "system",
248
+ ...baseEntry(rawCopy, timestamp, date, source),
249
+ raw: rawCopy,
250
+ } satisfies GenericEntry);
251
+ }
252
+
253
+ if (entries.length > 500) await new Promise<void>((r) => setImmediate(r));
254
+ entries.sort((a, b) => a.timestampMs - b.timestampMs);
255
+
256
+ return { entries, rawLines, cwd };
257
+ }
258
+
259
+ // ── Public loader ──
260
+
261
+ export interface PiSessionLogData {
262
+ entries: LogEntry[];
263
+ rawLines: Record<string, unknown>[];
264
+ cwd?: string;
265
+ filePath: string;
266
+ }
267
+
268
+ export async function getPiSessionLog(sessionId: string): Promise<PiSessionLogData | null> {
269
+ const filePath = findPiTranscript(sessionId);
270
+ if (!filePath) return null;
271
+ let fileContent: string;
272
+ try {
273
+ fileContent = await readFile(filePath, "utf-8");
274
+ } catch {
275
+ // The file vanished between findPiTranscript and read — fall open.
276
+ return null;
277
+ }
278
+ let parsed: PiParseResult;
279
+ try {
280
+ parsed = await parsePiLog(fileContent, "session");
281
+ } catch {
282
+ return null;
283
+ }
284
+ return {
285
+ entries: parsed.entries,
286
+ rawLines: parsed.rawLines,
287
+ cwd: parsed.cwd,
288
+ filePath,
289
+ };
290
+ }
291
+
292
+ export const getCachedPiSessionLog = runtimeCache(
293
+ (sessionId: string) => getPiSessionLog(sessionId),
294
+ 60,
295
+ { maxSize: 50 },
296
+ );
297
+
298
+ // ── Test helpers ──
299
+
300
+ /** For tests: read raw stat of the transcript path, returning null on miss. */
301
+ export function _statPiTranscript(sessionId: string): { mtimeMs: number } | null {
302
+ const path = findPiTranscript(sessionId);
303
+ if (!path) return null;
304
+ try {
305
+ const s = statSync(path);
306
+ return { mtimeMs: s.mtimeMs };
307
+ } catch {
308
+ return null;
309
+ }
310
+ }
311
+
312
+ /** For tests: read transcript synchronously. Returns null on missing/error. */
313
+ export function readPiTranscriptSync(sessionId: string): string | null {
314
+ const path = findPiTranscript(sessionId);
315
+ if (!path) return null;
316
+ try {
317
+ return readFileSync(path, "utf-8");
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+
323
+ /** Suppress unused-import warning for formatTimestamp; reserved for tool-call
324
+ * rendering once Pi emits it (see header comment). */
325
+ void formatTimestamp;
package/lib/projects.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * All functions return sorted arrays (newest-first) and pre-format dates
6
6
  * so that client components can display them without hydration mismatches.
7
7
  */
8
- import { readdir, stat } from "fs/promises";
9
- import { join, resolve, sep } from "path";
8
+ import { readdir, stat } from "node:fs/promises";
9
+ import { join, resolve, sep } from "node:path";
10
10
  import { getClaudeProjectsPath } from "./paths";
11
11
  import { runtimeCache } from "./runtime-cache";
12
12
  import { batchAll } from "./concurrency";
@@ -16,7 +16,7 @@ import { formatDate } from "./format-date";
16
16
  export const UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
17
17
  export const PATH_TRAVERSAL_RE = /(^|[\\/])\.\.($|[\\/])/;
18
18
 
19
- export type ProjectCli = "claude" | "codex";
19
+ export type ProjectCli = "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "gemini";
20
20
 
21
21
  export interface ProjectFolder {
22
22
  name: string;
@@ -26,7 +26,7 @@ export interface ProjectFolder {
26
26
  lastModifiedFormatted?: string; // Pre-formatted date string to avoid hydration issues
27
27
  /**
28
28
  * Which agent CLIs this project's data was found in. Multiple entries when
29
- * the same cwd has both Claude and Codex transcripts; rendered as badges.
29
+ * the same cwd has transcripts from more than one CLI; rendered as badges.
30
30
  */
31
31
  cli: ProjectCli[];
32
32
  }
@@ -37,8 +37,8 @@ export interface SessionFile {
37
37
  lastModified: Date;
38
38
  lastModifiedFormatted?: string;
39
39
  sessionId?: string;
40
- /** Originating agent CLI. Set when the session list mixes Claude + Codex sources
41
- * so the table can render a per-row CLI badge. */
40
+ /** Originating agent CLI. Set when the session list mixes sources from more
41
+ * than one CLI, so the table can render a per-row CLI badge. */
42
42
  cli?: ProjectCli;
43
43
  }
44
44
 
@@ -99,28 +99,30 @@ async function getClaudeProjectFolders(): Promise<ProjectFolder[]> {
99
99
  }
100
100
  }
101
101
 
102
- /** Merges Claude + Codex project lists by encoded folder name. When both sources have
103
- * the same name, keeps the Claude entry's `path` (so the Path column still points at
104
- * `~/.claude/projects/<encoded>`), unions the `cli` arrays in [claude, codex] order,
105
- * and takes the newer `lastModified`. */
106
- function mergeProjectFolders(claude: ProjectFolder[], codex: ProjectFolder[]): ProjectFolder[] {
102
+ /** Merges any number of per-CLI project lists by encoded folder name. When
103
+ * multiple sources share a name, the first source's `path` wins (so the Path
104
+ * column still points at the primary store), the `cli` arrays are unioned in
105
+ * source order, and `lastModified` takes the newest value. The first source
106
+ * is treated as the "base" — pass Claude first to keep Claude paths primary. */
107
+ function mergeProjectFolders(...sources: ProjectFolder[][]): ProjectFolder[] {
107
108
  const byName = new Map<string, ProjectFolder>();
108
- for (const f of claude) byName.set(f.name, { ...f, cli: [...f.cli] });
109
- for (const f of codex) {
110
- const existing = byName.get(f.name);
111
- if (!existing) {
112
- byName.set(f.name, { ...f, cli: [...f.cli] });
113
- continue;
109
+ for (const list of sources) {
110
+ for (const f of list) {
111
+ const existing = byName.get(f.name);
112
+ if (!existing) {
113
+ byName.set(f.name, { ...f, cli: [...f.cli] });
114
+ continue;
115
+ }
116
+ const mergedCli: ProjectCli[] = [...existing.cli];
117
+ for (const c of f.cli) if (!mergedCli.includes(c)) mergedCli.push(c);
118
+ const newer = f.lastModified.getTime() > existing.lastModified.getTime() ? f : existing;
119
+ byName.set(f.name, {
120
+ ...existing,
121
+ cli: mergedCli,
122
+ lastModified: newer.lastModified,
123
+ lastModifiedFormatted: newer.lastModifiedFormatted,
124
+ });
114
125
  }
115
- const mergedCli: ProjectCli[] = [...existing.cli];
116
- for (const c of f.cli) if (!mergedCli.includes(c)) mergedCli.push(c);
117
- const newer = f.lastModified.getTime() > existing.lastModified.getTime() ? f : existing;
118
- byName.set(f.name, {
119
- ...existing,
120
- cli: mergedCli,
121
- lastModified: newer.lastModified,
122
- lastModifiedFormatted: newer.lastModifiedFormatted,
123
- });
124
126
  }
125
127
  const merged = Array.from(byName.values());
126
128
  merged.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
@@ -128,17 +130,51 @@ function mergeProjectFolders(claude: ProjectFolder[], codex: ProjectFolder[]): P
128
130
  }
129
131
 
130
132
  export async function getProjectFolders(): Promise<ProjectFolder[]> {
131
- // Lazy import to keep `lib/codex-projects.ts` out of the dep graph for callers that
132
- // only need Claude helpers (e.g. CLI codepaths).
133
- const { getCodexProjects } = await import("./codex-projects");
134
- const [claude, codex] = await Promise.all([
133
+ // Lazy imports keep the per-CLI project providers out of the dep graph for
134
+ // callers that only need Claude helpers (e.g. CLI codepaths).
135
+ const [
136
+ { getCodexProjects },
137
+ { getCopilotProjects },
138
+ { getCursorProjects },
139
+ { getOpenCodeProjects },
140
+ { getPiProjects },
141
+ { getGeminiProjects },
142
+ ] = await Promise.all([
143
+ import("./codex-projects"),
144
+ import("./copilot-projects"),
145
+ import("./cursor-projects"),
146
+ import("./opencode-projects"),
147
+ import("./pi-projects"),
148
+ import("./gemini-projects"),
149
+ ]);
150
+ const [claude, codex, copilot, cursor, opencode, pi, gemini] = await Promise.all([
135
151
  getClaudeProjectFolders(),
136
152
  getCodexProjects().catch((error) => {
137
153
  logError("Error reading Codex projects:", error);
138
154
  return [] as ProjectFolder[];
139
155
  }),
156
+ getCopilotProjects().catch((error) => {
157
+ logError("Error reading Copilot projects:", error);
158
+ return [] as ProjectFolder[];
159
+ }),
160
+ getCursorProjects().catch((error) => {
161
+ logError("Error reading Cursor projects:", error);
162
+ return [] as ProjectFolder[];
163
+ }),
164
+ getOpenCodeProjects().catch((error) => {
165
+ logError("Error reading OpenCode projects:", error);
166
+ return [] as ProjectFolder[];
167
+ }),
168
+ getPiProjects().catch((error) => {
169
+ logError("Error reading Pi projects:", error);
170
+ return [] as ProjectFolder[];
171
+ }),
172
+ getGeminiProjects().catch((error) => {
173
+ logError("Error reading Gemini projects:", error);
174
+ return [] as ProjectFolder[];
175
+ }),
140
176
  ]);
141
- return mergeProjectFolders(claude, codex);
177
+ return mergeProjectFolders(claude, codex, copilot, cursor, opencode, pi, gemini);
142
178
  }
143
179
 
144
180
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.9",
3
+ "version": "0.0.10-beta.0",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"
@@ -10,6 +10,7 @@
10
10
  "src/",
11
11
  "scripts/",
12
12
  "lib/",
13
+ "pi-extension/",
13
14
  ".next/standalone/",
14
15
  "dist/",
15
16
  "README.md"