failproofai 0.0.9 → 0.0.10-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.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]__0_b7pgn._.js → [root-of-the-server]__0lkkjl_._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.js → [root-of-the-server]__0mb9b9d._.js} +2 -2
- 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/_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/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0ef3uwk.js +1 -1
- 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/{0n-_j_6fo6jex.js → 0-wd3kiz5wrsz.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0222q~_4u7p6h.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.js → 02y~6tp1j1wkh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 09qdljea8j.3~.js} +1 -1
- package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
- package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.js → 0pt38lwlsaxvs.js} +1 -1
- package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0u-ys71jc4y68.js → 0vl201wjmz17m.js} +2 -2
- package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.js → 0vl~p17i-4qt2.js} +1 -1
- package/.next/standalone/.next/static/chunks/0xkzmsj-sniqz.js +1 -0
- package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
- 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 +2405 -253
- 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/install-diagnosis.mjs +190 -0
- package/scripts/launch.ts +32 -0
- package/scripts/postinstall.mjs +25 -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/06x4-d1~o-opr.js +0 -1
- package/.next/standalone/.next/static/chunks/095l4hc7-h.~~.js +0 -1
- package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → w0GG7S5UEj1-p5g9hfsh2}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// AUTO-GENERATED by failproofai. __failproofai_hook__
|
|
2
|
+
// Re-generate via: failproofai policies --install --cli opencode
|
|
3
|
+
//
|
|
4
|
+
// DEV variant: this repo's contributors run failproofai from source via
|
|
5
|
+
// `bun bin/failproofai.mjs ...` instead of `npx -y failproofai`. This file
|
|
6
|
+
// is the project-scope shim with that dev path baked in. The production
|
|
7
|
+
// shape (npx-based) is generated by `src/hooks/integrations.ts` on install
|
|
8
|
+
// for end users — see `buildOpenCodePluginShim` there.
|
|
9
|
+
//
|
|
10
|
+
// Do NOT install this repo via `failproofai policies --install --cli
|
|
11
|
+
// opencode --scope project` — it would overwrite this dev path with the
|
|
12
|
+
// portable npx form.
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
|
|
16
|
+
const REPO_ROOT = resolve(import.meta.dirname, "..", "..");
|
|
17
|
+
const FAILPROOFAI_DEV_BIN = resolve(REPO_ROOT, "bin", "failproofai.mjs");
|
|
18
|
+
|
|
19
|
+
const BUS_EVENT_MAP = {
|
|
20
|
+
"session.created": "SessionStart",
|
|
21
|
+
"session.deleted": "SessionEnd",
|
|
22
|
+
"session.idle": "Stop",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function runFailproofai(eventName, payload, directory) {
|
|
26
|
+
const r = spawnSync("bun", [FAILPROOFAI_DEV_BIN, "--hook", eventName, "--cli", "opencode"], {
|
|
27
|
+
input: JSON.stringify(payload),
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
timeout: 60_000,
|
|
30
|
+
cwd: directory,
|
|
31
|
+
});
|
|
32
|
+
return { exitCode: r.status ?? 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function applyDecision(result, ctx) {
|
|
36
|
+
if (result.exitCode === 2) {
|
|
37
|
+
throw new Error((result.stderr || "").trim() || "Blocked by failproofai");
|
|
38
|
+
}
|
|
39
|
+
let parsed = null;
|
|
40
|
+
try { parsed = JSON.parse(result.stdout); } catch { /* fail-open allow */ }
|
|
41
|
+
if (!parsed) return;
|
|
42
|
+
const out = parsed.hookSpecificOutput;
|
|
43
|
+
if (out && out.permissionDecision === "deny") {
|
|
44
|
+
throw new Error(out.permissionDecisionReason || "Blocked by failproofai");
|
|
45
|
+
}
|
|
46
|
+
if (out && out.decision && out.decision.behavior === "deny") {
|
|
47
|
+
throw new Error((out.decision.message) || "Blocked by failproofai");
|
|
48
|
+
}
|
|
49
|
+
const ctxText = out && out.additionalContext;
|
|
50
|
+
if (ctxText && ctx && ctx.client && ctx.sessionID) {
|
|
51
|
+
Promise.resolve(ctx.client.session.prompt({
|
|
52
|
+
path: { id: ctx.sessionID },
|
|
53
|
+
body: { parts: [{ type: "text", text: ctxText }] },
|
|
54
|
+
})).catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default async function failproofaiPlugin({ client, directory }) {
|
|
59
|
+
return {
|
|
60
|
+
event: async ({ event }) => {
|
|
61
|
+
if (!event || !event.type) return;
|
|
62
|
+
if (event.type === "message.updated") {
|
|
63
|
+
const props = event.properties || {};
|
|
64
|
+
const info = props.info || props.message || {};
|
|
65
|
+
const role = info.role || props.role;
|
|
66
|
+
if (role !== "user") return;
|
|
67
|
+
const sessionID = info.sessionID || info.sessionId || info.session_id || props.sessionID;
|
|
68
|
+
// Reconstruct the user prompt text so prompt-based policies see it.
|
|
69
|
+
let prompt = "";
|
|
70
|
+
const parts = info.parts || props.parts || [];
|
|
71
|
+
if (Array.isArray(parts)) {
|
|
72
|
+
for (const p of parts) {
|
|
73
|
+
if (p && typeof p === "object" && typeof p.text === "string") prompt += p.text;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!prompt) prompt = (info.text || info.content || props.text || "").toString();
|
|
77
|
+
const r = runFailproofai("UserPromptSubmit", {
|
|
78
|
+
session_id: sessionID, cwd: directory, hook_event_name: "UserPromptSubmit", prompt,
|
|
79
|
+
}, directory);
|
|
80
|
+
applyDecision(r, { client, sessionID });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const claudeEvent = BUS_EVENT_MAP[event.type];
|
|
84
|
+
if (!claudeEvent) return;
|
|
85
|
+
const props = event.properties || {};
|
|
86
|
+
const sessionID = props.sessionID || (props.session && props.session.id) || props.id;
|
|
87
|
+
const r = runFailproofai(claudeEvent, {
|
|
88
|
+
session_id: sessionID, cwd: directory, hook_event_name: claudeEvent,
|
|
89
|
+
}, directory);
|
|
90
|
+
applyDecision(r, { client, sessionID });
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
"tool.execute.before": async (input, output) => {
|
|
94
|
+
const r = runFailproofai("PreToolUse", {
|
|
95
|
+
session_id: input.sessionID,
|
|
96
|
+
cwd: directory,
|
|
97
|
+
tool_name: input.tool,
|
|
98
|
+
tool_input: output.args,
|
|
99
|
+
hook_event_name: "PreToolUse",
|
|
100
|
+
}, directory);
|
|
101
|
+
applyDecision(r, { client, sessionID: input.sessionID });
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
"tool.execute.after": async (input, output) => {
|
|
105
|
+
const r = runFailproofai("PostToolUse", {
|
|
106
|
+
session_id: input.sessionID,
|
|
107
|
+
cwd: directory,
|
|
108
|
+
tool_name: input.tool,
|
|
109
|
+
tool_input: input.args,
|
|
110
|
+
tool_response: { title: output.title, output: output.output, metadata: output.metadata },
|
|
111
|
+
hook_event_name: "PostToolUse",
|
|
112
|
+
}, directory);
|
|
113
|
+
applyDecision(r, { client, sessionID: input.sessionID });
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
"permission.ask": async (input, output) => {
|
|
117
|
+
const r = runFailproofai("PermissionRequest", {
|
|
118
|
+
session_id: input.sessionID,
|
|
119
|
+
cwd: directory,
|
|
120
|
+
tool_name: input.tool || input.command || "permission",
|
|
121
|
+
tool_input: input,
|
|
122
|
+
hook_event_name: "PermissionRequest",
|
|
123
|
+
}, directory);
|
|
124
|
+
try {
|
|
125
|
+
applyDecision(r, { client, sessionID: input.sessionID });
|
|
126
|
+
} catch {
|
|
127
|
+
output.status = "deny";
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tiny CLI-origin badge
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* and session viewer.
|
|
2
|
+
* Tiny CLI-origin badge. Visual style and label are sourced from
|
|
3
|
+
* `lib/cli-registry.ts` so adding a new agent CLI = one registry entry, no UI
|
|
4
|
+
* changes here.
|
|
6
5
|
*/
|
|
7
6
|
import type { ProjectCli } from "@/lib/projects";
|
|
7
|
+
import { getCliLabel, getCliBadgeClasses } from "@/lib/cli-registry";
|
|
8
8
|
|
|
9
9
|
export function CliBadge({ cli }: { cli: ProjectCli }) {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const label = getCliLabel(cli);
|
|
11
|
+
const classes = getCliBadgeClasses(cli);
|
|
12
12
|
return (
|
|
13
13
|
<span
|
|
14
|
-
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium border ${
|
|
15
|
-
isCodex
|
|
16
|
-
? "bg-purple-500/10 text-purple-400 border-purple-500/20"
|
|
17
|
-
: "bg-orange-500/10 text-orange-400 border-orange-500/20"
|
|
18
|
-
}`}
|
|
14
|
+
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium border ${classes}`}
|
|
19
15
|
title={`Agent CLI: ${label}`}
|
|
20
16
|
>
|
|
21
17
|
{label}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
keywordsToParam, paramToKeywords,
|
|
24
24
|
pageToParam, paramToPage,
|
|
25
25
|
} from "@/lib/url-filter-serializers";
|
|
26
|
+
import { KNOWN_CLI_IDS, getCliLabel, isKnownCli, type CliId } from "@/lib/cli-registry";
|
|
26
27
|
import { Folder, Search, X } from "lucide-react";
|
|
27
28
|
import Link from "next/link";
|
|
28
29
|
import PaginationControls from "./pagination-controls";
|
|
@@ -50,6 +51,10 @@ export default function ProjectList({ folders }: ProjectListProps) {
|
|
|
50
51
|
// Read initial state from URL
|
|
51
52
|
const [keywords, setKeywords] = useState<string[]>(() => paramToKeywords(url.get("q")));
|
|
52
53
|
const [keywordInput, setKeywordInput] = useState("");
|
|
54
|
+
const [filterCli, setFilterCli] = useState<"" | CliId>(() => {
|
|
55
|
+
const v = url.get("cli");
|
|
56
|
+
return isKnownCli(v) ? v : "";
|
|
57
|
+
});
|
|
53
58
|
|
|
54
59
|
const {
|
|
55
60
|
filterPreset, dateRange, currentPage, setCurrentPage,
|
|
@@ -72,9 +77,10 @@ export default function ProjectList({ folders }: ProjectListProps) {
|
|
|
72
77
|
...dateRangeToParams(dateRange),
|
|
73
78
|
q: keywordsToParam(keywords),
|
|
74
79
|
page: pageToParam(currentPage),
|
|
80
|
+
cli: filterCli || undefined,
|
|
75
81
|
});
|
|
76
82
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
-
}, [filterPreset, dateRange, keywords, currentPage]);
|
|
83
|
+
}, [filterPreset, dateRange, keywords, currentPage, filterCli]);
|
|
78
84
|
|
|
79
85
|
const addKeyword = (keyword: string) => {
|
|
80
86
|
const trimmed = keyword.trim();
|
|
@@ -96,6 +102,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
|
|
|
96
102
|
const clearFilters = () => {
|
|
97
103
|
clearDateFilters();
|
|
98
104
|
clearKeywords();
|
|
105
|
+
setFilterCli("");
|
|
99
106
|
};
|
|
100
107
|
|
|
101
108
|
const normalizedFolders = useMemo(() => rehydrateDates(folders), [folders]);
|
|
@@ -113,8 +120,12 @@ export default function ProjectList({ folders }: ProjectListProps) {
|
|
|
113
120
|
});
|
|
114
121
|
}
|
|
115
122
|
|
|
123
|
+
if (filterCli) {
|
|
124
|
+
filtered = filtered.filter((folder) => folder.cli.includes(filterCli));
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
return filtered.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
117
|
-
}, [normalizedFolders, filterPreset, dateRange, keywords]);
|
|
128
|
+
}, [normalizedFolders, filterPreset, dateRange, keywords, filterCli]);
|
|
118
129
|
|
|
119
130
|
const totalPages = Math.max(1, Math.ceil(filteredFolders.length / ITEMS_PER_PAGE));
|
|
120
131
|
useEffect(() => {
|
|
@@ -129,7 +140,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
|
|
|
129
140
|
{/* Filter Bar */}
|
|
130
141
|
<div className="bg-card border border-border rounded-lg p-4">
|
|
131
142
|
<div className="flex flex-col gap-4">
|
|
132
|
-
{/* Preset Filters +
|
|
143
|
+
{/* Preset Filters + CLI filter */}
|
|
133
144
|
<div className="flex flex-wrap items-center gap-2">
|
|
134
145
|
<span className="text-sm font-medium text-foreground">Filter by:</span>
|
|
135
146
|
{FILTER_PRESETS.map((preset) => (
|
|
@@ -146,6 +157,23 @@ export default function ProjectList({ folders }: ProjectListProps) {
|
|
|
146
157
|
</button>
|
|
147
158
|
))}
|
|
148
159
|
|
|
160
|
+
<span className="ml-2 text-sm font-medium text-foreground">CLI:</span>
|
|
161
|
+
<select
|
|
162
|
+
aria-label="Filter by CLI"
|
|
163
|
+
value={filterCli}
|
|
164
|
+
onChange={(e) => {
|
|
165
|
+
const v = e.target.value;
|
|
166
|
+
setFilterCli(v === "" || isKnownCli(v) ? v : "");
|
|
167
|
+
}}
|
|
168
|
+
className="px-2 py-1.5 text-sm bg-input border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
|
|
169
|
+
>
|
|
170
|
+
<option value="">All CLIs</option>
|
|
171
|
+
{KNOWN_CLI_IDS.map((id) => (
|
|
172
|
+
<option key={id} value={id}>
|
|
173
|
+
{getCliLabel(id)}
|
|
174
|
+
</option>
|
|
175
|
+
))}
|
|
176
|
+
</select>
|
|
149
177
|
</div>
|
|
150
178
|
|
|
151
179
|
{/* Keyword Search */}
|
|
@@ -227,7 +255,7 @@ export default function ProjectList({ folders }: ProjectListProps) {
|
|
|
227
255
|
aria-label="Filter to date"
|
|
228
256
|
/>
|
|
229
257
|
</div>
|
|
230
|
-
{(filterPreset !== "all" || dateRange.from !== null || dateRange.to !== null || keywords.length > 0) && (
|
|
258
|
+
{(filterPreset !== "all" || dateRange.from !== null || dateRange.to !== null || keywords.length > 0 || filterCli !== "") && (
|
|
231
259
|
<button
|
|
232
260
|
onClick={clearFilters}
|
|
233
261
|
className="px-3 py-2 text-sm bg-muted text-muted-foreground hover:bg-muted/80 rounded-md transition-colors"
|
|
@@ -27,6 +27,7 @@ import { updatePolicyParamsAction } from "@/app/actions/update-policy-params";
|
|
|
27
27
|
import { useAutoRefresh } from "@/contexts/AutoRefreshContext";
|
|
28
28
|
import { useUrlParams } from "@/lib/use-url-params";
|
|
29
29
|
import { pageToParam, paramToPage } from "@/lib/url-filter-serializers";
|
|
30
|
+
import { getCliLabel, getCliBadgeClasses, KNOWN_CLI_IDS, isKnownCli, type CliId } from "@/lib/cli-registry";
|
|
30
31
|
import { formatRelativeTime } from "@/lib/format-duration";
|
|
31
32
|
import { Button } from "@/components/ui/button";
|
|
32
33
|
|
|
@@ -86,10 +87,24 @@ function SessionCell({
|
|
|
86
87
|
const short = shortenSession(sessionId);
|
|
87
88
|
|
|
88
89
|
const isCodex = integration === "codex" || (transcriptPath?.includes("/.codex/") ?? false);
|
|
89
|
-
|
|
90
|
+
const isCopilot =
|
|
91
|
+
integration === "copilot" ||
|
|
92
|
+
(transcriptPath?.includes("/.copilot/session-state/") ?? false);
|
|
93
|
+
const isCursor =
|
|
94
|
+
integration === "cursor" || (transcriptPath?.includes("/.cursor/") ?? false);
|
|
95
|
+
const isOpenCode =
|
|
96
|
+
integration === "opencode" ||
|
|
97
|
+
(transcriptPath?.includes("/.local/share/opencode/") ?? false) ||
|
|
98
|
+
(transcriptPath?.includes("/.opencode/") ?? false);
|
|
99
|
+
const isPi =
|
|
100
|
+
integration === "pi" || (transcriptPath?.includes("/.pi/") ?? false);
|
|
101
|
+
const isGemini =
|
|
102
|
+
integration === "gemini" || (transcriptPath?.includes("/.gemini/") ?? false);
|
|
103
|
+
if (isCodex || isCopilot || isCursor || isOpenCode || isPi || isGemini) {
|
|
90
104
|
// The session route auto-detects CLI by file location, so [name] only
|
|
91
105
|
// affects the breadcrumb. Encode the cwd Claude-style when we have it.
|
|
92
|
-
const
|
|
106
|
+
const fallbackSeg = isCodex ? "codex" : isCopilot ? "copilot" : isCursor ? "cursor" : isOpenCode ? "opencode" : isPi ? "pi" : "gemini";
|
|
107
|
+
const projectSeg = cwd ? encodeCwdForUrl(cwd) : fallbackSeg;
|
|
93
108
|
return (
|
|
94
109
|
<Link
|
|
95
110
|
href={`/project/${encodeURIComponent(projectSeg)}/session/${encodeURIComponent(sessionId)}`}
|
|
@@ -153,16 +168,11 @@ function EventTypeBadge({ eventType }: { eventType: string }) {
|
|
|
153
168
|
|
|
154
169
|
function IntegrationBadge({ integration }: { integration?: string }) {
|
|
155
170
|
if (!integration) return null;
|
|
156
|
-
const label =
|
|
157
|
-
|
|
158
|
-
const isCodex = integration === "codex";
|
|
171
|
+
const label = getCliLabel(integration);
|
|
172
|
+
const classes = getCliBadgeClasses(integration);
|
|
159
173
|
return (
|
|
160
174
|
<span
|
|
161
|
-
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium border ${
|
|
162
|
-
isCodex
|
|
163
|
-
? "bg-purple-500/10 text-purple-400 border-purple-500/20"
|
|
164
|
-
: "bg-orange-500/10 text-orange-400 border-orange-500/20"
|
|
165
|
-
}`}
|
|
175
|
+
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.6rem] font-medium border ${classes}`}
|
|
166
176
|
title={`Agent CLI: ${label}`}
|
|
167
177
|
>
|
|
168
178
|
{label}
|
|
@@ -366,9 +376,9 @@ function ActivityTab({
|
|
|
366
376
|
const [filterEventType, setFilterEventType] = useState(() => url.get("event") ?? "");
|
|
367
377
|
const [filterPolicy, setFilterPolicy] = useState(() => url.get("policy") ?? "");
|
|
368
378
|
const [filterSessionId, setFilterSessionId] = useState(() => url.get("session") ?? "");
|
|
369
|
-
const [filterCli, setFilterCli] = useState<"" |
|
|
379
|
+
const [filterCli, setFilterCli] = useState<"" | CliId>(() => {
|
|
370
380
|
const v = url.get("cli");
|
|
371
|
-
return v
|
|
381
|
+
return isKnownCli(v) ? v : "";
|
|
372
382
|
});
|
|
373
383
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
374
384
|
const filtersRef = useRef({ filterDecision, filterEventType, filterPolicy, filterSessionId, filterCli });
|
|
@@ -470,13 +480,19 @@ function ActivityTab({
|
|
|
470
480
|
</select>
|
|
471
481
|
<select
|
|
472
482
|
value={filterCli}
|
|
473
|
-
onChange={(e) =>
|
|
483
|
+
onChange={(e) => {
|
|
484
|
+
const v = e.target.value;
|
|
485
|
+
setFilterCli(v === "" || isKnownCli(v) ? v : "");
|
|
486
|
+
}}
|
|
474
487
|
className="h-7 rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-primary/40 focus:border-primary/40 transition-shadow"
|
|
475
488
|
aria-label="Filter by CLI"
|
|
476
489
|
>
|
|
477
490
|
<option value="">All CLIs</option>
|
|
478
|
-
|
|
479
|
-
|
|
491
|
+
{KNOWN_CLI_IDS.map((id) => (
|
|
492
|
+
<option key={id} value={id}>
|
|
493
|
+
{getCliLabel(id)}
|
|
494
|
+
</option>
|
|
495
|
+
))}
|
|
480
496
|
</select>
|
|
481
497
|
<div className="relative">
|
|
482
498
|
<input
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { Suspense } from "react";
|
|
3
3
|
import { resolveProjectPath, getCachedSessionFiles, type SessionFile } from "@/lib/projects";
|
|
4
4
|
import { getCachedCodexSessionsByEncodedName } from "@/lib/codex-projects";
|
|
5
|
+
import { getCachedCopilotSessionsByEncodedName } from "@/lib/copilot-projects";
|
|
6
|
+
import { getCachedCursorSessionsByEncodedName } from "@/lib/cursor-projects";
|
|
7
|
+
import { getCachedOpenCodeSessionsByEncodedName } from "@/lib/opencode-projects";
|
|
8
|
+
import { getCachedPiSessionsByEncodedName } from "@/lib/pi-projects";
|
|
9
|
+
import { getCachedGeminiSessionsByEncodedName } from "@/lib/gemini-projects";
|
|
5
10
|
import { logWarn } from "@/lib/logger";
|
|
6
11
|
import { decodeFolderName } from "@/lib/paths";
|
|
7
12
|
import { notFound } from "next/navigation";
|
|
@@ -23,7 +28,8 @@ interface ProjectPageProps {
|
|
|
23
28
|
export default async function ProjectPage({ params }: ProjectPageProps) {
|
|
24
29
|
const { name } = await params;
|
|
25
30
|
// Resolve under ~/.claude/projects/. Validation may throw RangeError; on bad input
|
|
26
|
-
// we still want to try
|
|
31
|
+
// we still want to try the external CLIs, since a non-Claude-only cwd never
|
|
32
|
+
// escapes this check.
|
|
27
33
|
let claudeProjectPath: string | null = null;
|
|
28
34
|
try {
|
|
29
35
|
claudeProjectPath = resolveProjectPath(name);
|
|
@@ -39,18 +45,39 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
|
|
39
45
|
claudeSessions = await getCachedSessionFiles(claudeProjectPath);
|
|
40
46
|
}
|
|
41
47
|
// Note: decodeFolderName is lossy when cwds contain `-` (every `-` becomes `/`),
|
|
42
|
-
// so
|
|
43
|
-
const codex = await
|
|
48
|
+
// so each external CLI looks up sessions by re-encoding cwd and matching the slug.
|
|
49
|
+
const [codex, copilot, cursor, opencode, pi, gemini] = await Promise.all([
|
|
50
|
+
getCachedCodexSessionsByEncodedName(name),
|
|
51
|
+
getCachedCopilotSessionsByEncodedName(name),
|
|
52
|
+
getCachedCursorSessionsByEncodedName(name),
|
|
53
|
+
getCachedOpenCodeSessionsByEncodedName(name),
|
|
54
|
+
getCachedPiSessionsByEncodedName(name),
|
|
55
|
+
getCachedGeminiSessionsByEncodedName(name),
|
|
56
|
+
]);
|
|
44
57
|
const codexSessions = codex.sessions;
|
|
58
|
+
const copilotSessions = copilot.sessions;
|
|
59
|
+
const cursorSessions = cursor.sessions;
|
|
60
|
+
const opencodeSessions = opencode.sessions;
|
|
61
|
+
const piSessions = pi.sessions;
|
|
62
|
+
const geminiSessions = gemini.sessions;
|
|
45
63
|
|
|
46
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
!claudeExists &&
|
|
66
|
+
codexSessions.length === 0 &&
|
|
67
|
+
copilotSessions.length === 0 &&
|
|
68
|
+
cursorSessions.length === 0 &&
|
|
69
|
+
opencodeSessions.length === 0 &&
|
|
70
|
+
piSessions.length === 0 &&
|
|
71
|
+
geminiSessions.length === 0
|
|
72
|
+
) {
|
|
47
73
|
notFound();
|
|
48
74
|
}
|
|
49
75
|
|
|
50
|
-
// Prefer
|
|
51
|
-
// ambiguous for cwds containing `-` (every `-`
|
|
52
|
-
//
|
|
53
|
-
|
|
76
|
+
// Prefer a canonical cwd recovered from any external store when available —
|
|
77
|
+
// `decodeFolderName(name)` is ambiguous for cwds containing `-` (every `-`
|
|
78
|
+
// becomes `/`). Each external transcript records the literal cwd, so they
|
|
79
|
+
// round-trip correctly. First non-null wins (Codex → Copilot → Cursor → OpenCode → Pi → Gemini).
|
|
80
|
+
const canonicalRoot = codex.cwd ?? copilot.cwd ?? cursor.cwd ?? opencode.cwd ?? pi.cwd ?? gemini.cwd ?? decodedName;
|
|
54
81
|
|
|
55
82
|
// Project header metadata
|
|
56
83
|
let lastModified: Date | null = null;
|
|
@@ -64,18 +91,27 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
|
|
64
91
|
logWarn(`Failed to get stats for project ${decodedName}:`, error);
|
|
65
92
|
}
|
|
66
93
|
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
const newestExternal = [codexSessions[0], copilotSessions[0], cursorSessions[0], opencodeSessions[0], piSessions[0], geminiSessions[0]]
|
|
95
|
+
.filter((s): s is SessionFile => !!s)
|
|
96
|
+
.map((s) => s.lastModified)
|
|
97
|
+
.reduce<Date | null>((acc, d) => (!acc || d.getTime() > acc.getTime() ? d : acc), null);
|
|
98
|
+
if (newestExternal && (!lastModified || newestExternal.getTime() > lastModified.getTime())) {
|
|
99
|
+
lastModified = newestExternal;
|
|
100
|
+
lastModifiedFormatted = formatDate(newestExternal);
|
|
71
101
|
}
|
|
72
102
|
|
|
73
|
-
const sessionFiles: SessionFile[] = [
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
const sessionFiles: SessionFile[] = [
|
|
104
|
+
...claudeSessions,
|
|
105
|
+
...codexSessions,
|
|
106
|
+
...copilotSessions,
|
|
107
|
+
...cursorSessions,
|
|
108
|
+
...opencodeSessions,
|
|
109
|
+
...piSessions,
|
|
110
|
+
...geminiSessions,
|
|
111
|
+
].sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
76
112
|
|
|
77
113
|
// Path line: prefer the Claude storage dir if present (matches existing UX);
|
|
78
|
-
// otherwise show the canonical
|
|
114
|
+
// otherwise show the canonical cwd recovered from the first external store.
|
|
79
115
|
const displayPath = claudeExists && claudeProjectPath ? claudeProjectPath : canonicalRoot;
|
|
80
116
|
|
|
81
117
|
return (
|
|
@@ -4,6 +4,11 @@ import { ArrowLeft, Download } from "lucide-react";
|
|
|
4
4
|
import { notFound } from "next/navigation";
|
|
5
5
|
import { getCachedSessionLog, type LogEntry } from "@/lib/log-entries";
|
|
6
6
|
import { getCachedCodexSessionLog } from "@/lib/codex-sessions";
|
|
7
|
+
import { getCachedCopilotSessionLog } from "@/lib/copilot-sessions";
|
|
8
|
+
import { getCachedCursorSessionLog } from "@/lib/cursor-sessions";
|
|
9
|
+
import { getCachedOpenCodeSessionLog } from "@/lib/opencode-sessions";
|
|
10
|
+
import { getCachedPiSessionLog } from "@/lib/pi-sessions";
|
|
11
|
+
import { getCachedGeminiSessionLog } from "@/lib/gemini-sessions";
|
|
7
12
|
import { decodeFolderName } from "@/lib/paths";
|
|
8
13
|
import { baseSessionId } from "@/lib/utils/session-id";
|
|
9
14
|
import { resolveProjectPath, UUID_RE } from "@/lib/projects";
|
|
@@ -30,13 +35,17 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|
|
30
35
|
}
|
|
31
36
|
const decodedName = decodeFolderName(name);
|
|
32
37
|
const decodedSessionId = baseSessionId(sessionId);
|
|
33
|
-
|
|
38
|
+
// OpenCode session IDs are not UUIDs — they use `ses_*` prefixes (e.g.
|
|
39
|
+
// `ses_21ad60d14ffewMeRRKMLdS7vOI`). The other four CLIs use UUIDs. Accept
|
|
40
|
+
// either; the per-CLI loader returns null for unknown IDs anyway.
|
|
41
|
+
const OPENCODE_SESSION_RE = /^ses_[A-Za-z0-9]+$/;
|
|
42
|
+
if (!UUID_RE.test(decodedSessionId) && !OPENCODE_SESSION_RE.test(decodedSessionId)) notFound();
|
|
34
43
|
|
|
35
44
|
let entries: LogEntry[] | null = null;
|
|
36
45
|
let rawLines: Record<string, unknown>[] | null = null;
|
|
37
46
|
let error: string | null = null;
|
|
38
|
-
let cli: "claude" | "codex" = "claude";
|
|
39
|
-
let
|
|
47
|
+
let cli: "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "gemini" = "claude";
|
|
48
|
+
let externalCwd: string | undefined;
|
|
40
49
|
|
|
41
50
|
try {
|
|
42
51
|
// Use raw folder name for file operations — decodedName is for display only
|
|
@@ -46,36 +55,89 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|
|
46
55
|
} catch (e) {
|
|
47
56
|
const isNotFound = (e as NodeJS.ErrnoException).code === "ENOENT";
|
|
48
57
|
if (isNotFound) {
|
|
49
|
-
// Fall back
|
|
50
|
-
//
|
|
51
|
-
//
|
|
58
|
+
// Fall back through external stores in order: Codex → Copilot → Cursor → OpenCode → Pi.
|
|
59
|
+
// Each store keys by sessionId rather than the project slug, so the
|
|
60
|
+
// [name] segment is irrelevant on these branches.
|
|
52
61
|
const codex = await getCachedCodexSessionLog(decodedSessionId);
|
|
53
62
|
if (codex) {
|
|
54
63
|
entries = codex.entries;
|
|
55
64
|
rawLines = codex.rawLines;
|
|
56
|
-
|
|
65
|
+
externalCwd = codex.cwd;
|
|
57
66
|
cli = "codex";
|
|
58
67
|
} else {
|
|
59
|
-
|
|
68
|
+
const copilot = await getCachedCopilotSessionLog(decodedSessionId);
|
|
69
|
+
if (copilot) {
|
|
70
|
+
entries = copilot.entries;
|
|
71
|
+
rawLines = copilot.rawLines;
|
|
72
|
+
externalCwd = copilot.cwd;
|
|
73
|
+
cli = "copilot";
|
|
74
|
+
} else {
|
|
75
|
+
const cursor = await getCachedCursorSessionLog(decodedSessionId);
|
|
76
|
+
if (cursor) {
|
|
77
|
+
entries = cursor.entries;
|
|
78
|
+
rawLines = cursor.rawLines;
|
|
79
|
+
externalCwd = cursor.cwd;
|
|
80
|
+
cli = "cursor";
|
|
81
|
+
} else {
|
|
82
|
+
const opencode = await getCachedOpenCodeSessionLog(decodedSessionId);
|
|
83
|
+
if (opencode) {
|
|
84
|
+
entries = opencode.entries;
|
|
85
|
+
rawLines = opencode.rawLines;
|
|
86
|
+
externalCwd = opencode.cwd;
|
|
87
|
+
cli = "opencode";
|
|
88
|
+
} else {
|
|
89
|
+
const pi = await getCachedPiSessionLog(decodedSessionId);
|
|
90
|
+
if (pi) {
|
|
91
|
+
entries = pi.entries;
|
|
92
|
+
rawLines = pi.rawLines;
|
|
93
|
+
externalCwd = pi.cwd;
|
|
94
|
+
cli = "pi";
|
|
95
|
+
} else {
|
|
96
|
+
const gemini = await getCachedGeminiSessionLog(decodedSessionId);
|
|
97
|
+
if (gemini) {
|
|
98
|
+
entries = gemini.entries;
|
|
99
|
+
rawLines = gemini.rawLines;
|
|
100
|
+
externalCwd = gemini.cwd;
|
|
101
|
+
cli = "gemini";
|
|
102
|
+
} else {
|
|
103
|
+
error = "Session log file not found.";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
60
109
|
}
|
|
61
110
|
} else {
|
|
62
111
|
error = "Failed to read session log.";
|
|
63
112
|
}
|
|
64
113
|
}
|
|
65
114
|
|
|
66
|
-
const
|
|
67
|
-
const headerLabel =
|
|
68
|
-
const headerValue =
|
|
115
|
+
const isExternal = cli !== "claude";
|
|
116
|
+
const headerLabel = isExternal ? "CLI" : "Project";
|
|
117
|
+
const headerValue =
|
|
118
|
+
cli === "codex"
|
|
119
|
+
? `OpenAI Codex${externalCwd ? ` · ${externalCwd}` : ""}`
|
|
120
|
+
: cli === "copilot"
|
|
121
|
+
? `GitHub Copilot${externalCwd ? ` · ${externalCwd}` : ""}`
|
|
122
|
+
: cli === "cursor"
|
|
123
|
+
? `Cursor Agent${externalCwd ? ` · ${externalCwd}` : ""}`
|
|
124
|
+
: cli === "opencode"
|
|
125
|
+
? `OpenCode${externalCwd ? ` · ${externalCwd}` : ""}`
|
|
126
|
+
: cli === "pi"
|
|
127
|
+
? `Pi${externalCwd ? ` · ${externalCwd}` : ""}`
|
|
128
|
+
: cli === "gemini"
|
|
129
|
+
? `Gemini CLI${externalCwd ? ` · ${externalCwd}` : ""}`
|
|
130
|
+
: decodedName;
|
|
69
131
|
|
|
70
132
|
return (
|
|
71
133
|
<main className="min-h-screen bg-background">
|
|
72
134
|
<div className="container mx-auto p-8">
|
|
73
135
|
<Link
|
|
74
|
-
href={
|
|
136
|
+
href={isExternal ? "/policies?tab=activity" : `/project/${encodeURIComponent(name)}`}
|
|
75
137
|
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground mb-6 transition-colors"
|
|
76
138
|
>
|
|
77
139
|
<ArrowLeft className="w-4 h-4" />
|
|
78
|
-
<span>{
|
|
140
|
+
<span>{isExternal ? "Back to Activity" : "Back to Sessions"}</span>
|
|
79
141
|
</Link>
|
|
80
142
|
|
|
81
143
|
<div className="mb-8">
|
|
@@ -99,7 +161,7 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|
|
99
161
|
<p className="text-muted-foreground">
|
|
100
162
|
<span className="font-medium">{rawLines.length}</span> log lines
|
|
101
163
|
</p>
|
|
102
|
-
{!
|
|
164
|
+
{!isExternal && (
|
|
103
165
|
<a
|
|
104
166
|
href={`/api/download/${encodeURIComponent(name)}/${encodeURIComponent(decodedSessionId)}`}
|
|
105
167
|
download
|
|
@@ -122,7 +184,22 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|
|
122
184
|
{!error && entries && (
|
|
123
185
|
<LazyLogViewer
|
|
124
186
|
entries={entries}
|
|
125
|
-
projectName={
|
|
187
|
+
projectName={
|
|
188
|
+
isExternal
|
|
189
|
+
? (externalCwd ??
|
|
190
|
+
(cli === "codex"
|
|
191
|
+
? "OpenAI Codex"
|
|
192
|
+
: cli === "copilot"
|
|
193
|
+
? "GitHub Copilot"
|
|
194
|
+
: cli === "cursor"
|
|
195
|
+
? "Cursor Agent"
|
|
196
|
+
: cli === "opencode"
|
|
197
|
+
? "OpenCode"
|
|
198
|
+
: cli === "pi"
|
|
199
|
+
? "Pi"
|
|
200
|
+
: "Gemini CLI"))
|
|
201
|
+
: decodedName
|
|
202
|
+
}
|
|
126
203
|
sessionId={decodedSessionId}
|
|
127
204
|
/>
|
|
128
205
|
)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF"><title>GitHub Copilot</title><path d="M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000"><title>GitHub Copilot</title><path d="M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="395 393 175 197" role="img"><title>Cursor Agent</title><path fill="#72716D" d="M483.395 490.5L566 538.297C565.493 539.178 564.757 539.93 563.845 540.456L486.636 585.13C484.632 586.29 482.159 586.29 480.154 585.13L402.945 540.456C402.034 539.93 401.297 539.178 400.79 538.297L483.395 490.5Z"/><path fill="#55544F" d="M483.395 395V490.5L400.79 538.297C400.282 537.416 400 536.398 400 535.346V445.654C400 443.545 401.122 441.6 402.945 440.544L480.15 395.87C481.154 395.29 482.273 395 483.391 395H483.395Z"/><path fill="#43413C" d="M565.996 442.703C565.489 441.822 564.752 441.07 563.841 440.544L486.632 395.87C485.632 395.29 484.513 395 483.395 395V490.5L566 538.297C566.507 537.416 566.789 536.398 566.789 535.346V445.654C566.789 444.598 566.511 443.588 566 442.703H565.996Z"/><path fill="#D6D5D2" d="M560.218 446.049C560.686 446.858 560.751 447.896 560.218 448.82L485.235 578.974C484.732 579.855 483.392 579.493 483.392 578.479V492.713C483.392 492.029 483.209 491.37 482.877 490.794L560.215 446.045H560.218V446.049Z"/><path fill="#FFFFFF" d="M560.218 446.049L482.88 490.797C482.552 490.224 482.073 489.737 481.48 489.394L407.369 446.511C406.49 446.006 406.851 444.663 407.862 444.663H557.824C558.889 444.663 559.754 445.239 560.218 446.049Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="395 393 175 197" role="img"><title>Cursor Agent</title><path fill="#72716D" d="M483.395 490.5L566 538.297C565.493 539.178 564.757 539.93 563.845 540.456L486.636 585.13C484.632 586.29 482.159 586.29 480.154 585.13L402.945 540.456C402.034 539.93 401.297 539.178 400.79 538.297L483.395 490.5Z"/><path fill="#55544F" d="M483.395 395V490.5L400.79 538.297C400.282 537.416 400 536.398 400 535.346V445.654C400 443.545 401.122 441.6 402.945 440.544L480.15 395.87C481.154 395.29 482.273 395 483.391 395H483.395Z"/><path fill="#43413C" d="M565.996 442.703C565.489 441.822 564.752 441.07 563.841 440.544L486.632 395.87C485.632 395.29 484.513 395 483.395 395V490.5L566 538.297C566.507 537.416 566.789 536.398 566.789 535.346V445.654C566.789 444.598 566.511 443.588 566 442.703H565.996Z"/><path fill="#D6D5D2" d="M560.218 446.049C560.686 446.858 560.751 447.896 560.218 448.82L485.235 578.974C484.732 579.855 483.392 579.493 483.392 578.479V492.713C483.392 492.029 483.209 491.37 482.877 490.794L560.215 446.045H560.218V446.049Z"/><path fill="#FFFFFF" d="M560.218 446.049L482.88 490.797C482.552 490.224 482.073 489.737 481.48 489.394L407.369 446.511C406.49 446.006 406.851 444.663 407.862 444.663H557.824C558.889 444.663 559.754 445.239 560.218 446.049Z"/></svg>
|