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.
- package/.next/standalone/.cursor/hooks.json +47 -0
- package/.next/standalone/.gemini/settings.json +147 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.~nmr9._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0yspgjy._.js → [root-of-the-server]__010i6f5._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08px0ym._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kjo7d_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wu7fr7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z4c5dj._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zso~62._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09icjsf._.js → [root-of-the-server]__0709m8.._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vrlxf2._.js → [root-of-the-server]__0ymn496._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0eu4j_n._.js → [root-of-the-server]__10h.ggz._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_07a1g.3._.js → _0zx~s__._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_copilot-projects_ts_0r8xkn8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_cursor-projects_ts_0qt1scg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_pi-projects_ts_103tsh1._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/static/chunks/{06j6c0ofqjy0v.js → 00ay03h8bq4b~.js} +2 -2
- package/.next/standalone/.next/static/chunks/{15_mi91qaeieu.js → 0agmlhk5ml7x5.js} +1 -1
- package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
- package/.next/standalone/.next/static/chunks/0en4v5k2nnxks.js +1 -0
- package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0zdn~84f58hvf.js → 0s6nux54y~l~r.js} +1 -1
- package/.next/standalone/.next/static/chunks/{10mlwc4y_kqo2.js → 0tpse0wu2wwo0.js} +1 -1
- package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
- package/.next/standalone/.next/static/chunks/{0e8c_1f7-8e7t.js → 1400rtd5ywbt..js} +2 -2
- package/.next/standalone/.next/static/chunks/{0mumk7h5i.1xd.js → 14lmf8boay-zu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18a9xv2p3~x.9.js → 17htukxga7bil.js} +1 -1
- package/.next/standalone/.opencode/opencode.json +4 -0
- package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
- package/.next/standalone/.pi/settings.json +5 -0
- package/.next/standalone/app/components/cli-badge.tsx +7 -11
- package/.next/standalone/app/components/project-list.tsx +32 -4
- package/.next/standalone/app/policies/hooks-client.tsx +31 -15
- package/.next/standalone/app/project/[name]/page.tsx +52 -16
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
- package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
- package/.next/standalone/assets/logos/copilot-light.svg +1 -0
- package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
- package/.next/standalone/assets/logos/cursor-light.svg +1 -0
- package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
- package/.next/standalone/assets/logos/gemini-light.svg +13 -0
- package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
- package/.next/standalone/assets/logos/opencode-light.svg +1 -0
- package/.next/standalone/assets/logos/pi-dark.svg +7 -0
- package/.next/standalone/assets/logos/pi-light.svg +7 -0
- package/.next/standalone/lib/cli-registry.ts +107 -0
- package/.next/standalone/lib/codex-projects.ts +3 -3
- package/.next/standalone/lib/copilot-projects.ts +224 -0
- package/.next/standalone/lib/copilot-sessions.ts +395 -0
- package/.next/standalone/lib/cursor-projects.ts +312 -0
- package/.next/standalone/lib/cursor-sessions.ts +467 -0
- package/.next/standalone/lib/gemini-projects.ts +203 -0
- package/.next/standalone/lib/gemini-sessions.ts +365 -0
- package/.next/standalone/lib/opencode-projects.ts +232 -0
- package/.next/standalone/lib/opencode-sessions.ts +237 -0
- package/.next/standalone/lib/pi-projects.ts +230 -0
- package/.next/standalone/lib/pi-sessions.ts +325 -0
- package/.next/standalone/lib/projects.ts +67 -31
- package/.next/standalone/next.config.ts +5 -4
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/pi-extension/index.ts +373 -0
- package/.next/standalone/pi-extension/package.json +12 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +37 -3
- package/bin/failproofai.mjs +61 -21
- package/dist/cli.mjs +2248 -246
- package/lib/cli-registry.ts +107 -0
- package/lib/codex-projects.ts +3 -3
- package/lib/copilot-projects.ts +224 -0
- package/lib/copilot-sessions.ts +395 -0
- package/lib/cursor-projects.ts +312 -0
- package/lib/cursor-sessions.ts +467 -0
- package/lib/gemini-projects.ts +203 -0
- package/lib/gemini-sessions.ts +365 -0
- package/lib/opencode-projects.ts +232 -0
- package/lib/opencode-sessions.ts +237 -0
- package/lib/pi-projects.ts +230 -0
- package/lib/pi-sessions.ts +325 -0
- package/lib/projects.ts +67 -31
- package/package.json +2 -1
- package/pi-extension/index.ts +373 -0
- package/pi-extension/package.json +12 -0
- package/scripts/translate-docs/mdx-translator.ts +56 -2
- package/scripts/translate-docs/translator.ts +1 -1
- package/src/hooks/builtin-policies.ts +84 -14
- package/src/hooks/handler.ts +67 -5
- package/src/hooks/install-prompt.ts +33 -10
- package/src/hooks/integrations.ts +1007 -6
- package/src/hooks/policy-evaluator.ts +299 -3
- package/src/hooks/resolve-permission-mode.ts +23 -0
- package/src/hooks/types.ts +307 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
- package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
- package/.next/standalone/.next/static/chunks/03egp37o1l629.js +0 -1
- package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
- package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
- /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 68TLSFdjAQYIulNHfP0QY}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 68TLSFdjAQYIulNHfP0QY}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 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;
|
|
@@ -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
|
|
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
|
|
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
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* and takes the
|
|
106
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
132
|
-
// only need Claude helpers (e.g. CLI codepaths).
|
|
133
|
-
const
|
|
134
|
-
|
|
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
|
/**
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
// Static import (instead of `readFileSync(join(__dirname, "package.json"))`)
|
|
3
|
+
// keeps Turbopack's Node File Tracer from flagging this file as doing
|
|
4
|
+
// dynamic filesystem work, which produced an "Encountered unexpected file
|
|
5
|
+
// in NFT list" warning during `bun run build`.
|
|
6
|
+
import pkg from "./package.json";
|
|
6
7
|
|
|
7
8
|
const allowedDevOrigins = process.env.FAILPROOFAI_ALLOWED_DEV_ORIGINS
|
|
8
9
|
? process.env.FAILPROOFAI_ALLOWED_DEV_ORIGINS.split(",").map((s) => s.trim()).filter(Boolean)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.
|
|
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"
|