failproofai 0.0.10-beta.9 → 0.0.10
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/.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.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.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 +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- 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 +10 -10
- 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 +1 -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 +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- 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.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 +1 -1
- 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 +1 -1
- 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.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]__0fjhqi9._.js → [root-of-the-server]__044xt9.._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_ob4n._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +1 -1
- 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]__07_-mkc._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e9o9ri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0mnba92._.js → [root-of-the-server]__0l6swv1._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0logebz._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mi5ejy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0podumr._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rkxer-._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rl2kwi._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vg0uey._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ye1w50._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +32 -7
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__10._f0s._.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_gemini-projects_ts_0sl~yqr._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- 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/{0.a7kxwvbre7q.js → 0j171xiqge4rv.js} +1 -1
- package/.next/standalone/.next/static/chunks/0kqar56yl~41o.js +6 -0
- package/.next/standalone/.next/static/chunks/{0bndjr3n70vjn.js → 0lt8ko3lw.5yt.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0el2m08z2u5jm.js → 0ml1.ck_5t36i.js} +1 -1
- package/.next/standalone/.next/static/chunks/{11zd_~~dqyu39.js → 0pkl..xgo-qox.js} +1 -1
- package/.next/standalone/.next/static/chunks/12l2t63hkyo2q.js +1 -0
- package/.next/standalone/.next/static/chunks/{0-17za-4x~4gj.js → 14lii11wmo450.js} +1 -1
- package/.next/standalone/.next/static/chunks/{05i2pvqx75mg1.js → 179yytvmam0ug.js} +1 -1
- package/.next/standalone/.next/static/chunks/17rm86uz2nd5a.css +2 -0
- package/.next/standalone/.opencode/plugins/failproofai.mjs +75 -15
- package/.next/standalone/app/actions/get-hooks-config.ts +25 -1
- package/.next/standalone/app/policies/hooks-client.tsx +228 -44
- package/.next/standalone/lib/gemini-projects.ts +64 -24
- package/.next/standalone/lib/opencode-projects.ts +9 -7
- package/.next/standalone/package.json +2 -2
- package/.next/standalone/pi-extension/index.ts +113 -12
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +193 -68
- package/lib/gemini-projects.ts +64 -24
- package/lib/opencode-projects.ts +9 -7
- package/package.json +2 -2
- package/pi-extension/index.ts +113 -12
- package/scripts/launch.ts +5 -29
- package/src/hooks/handler.ts +63 -6
- package/src/hooks/integrations.ts +31 -6
- package/src/hooks/policy-evaluator.ts +34 -2
- package/src/hooks/types.ts +52 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.2-_y.._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okpm5k._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zwce~o._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0~tyclf._.js +0 -3
- package/.next/standalone/.next/static/chunks/0xd2k83e47~cx.js +0 -6
- package/.next/standalone/.next/static/chunks/0ywdtmk13p9_i.css +0 -2
- /package/.next/standalone/.next/static/{rOMtBtMm3y5vK0uC-Nzvi → dAuQps6jUwCz9X1Q5FFOO}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{rOMtBtMm3y5vK0uC-Nzvi → dAuQps6jUwCz9X1Q5FFOO}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{rOMtBtMm3y5vK0uC-Nzvi → dAuQps6jUwCz9X1Q5FFOO}/_ssgManifest.js +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -16,7 +16,7 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
18
|
// src/hooks/types.ts
|
|
19
|
-
var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, CODEX_TOOL_MAP, COPILOT_HOOK_SCOPES, COPILOT_HOOK_EVENT_TYPES, COPILOT_TOOL_MAP, CURSOR_HOOK_SCOPES, CURSOR_HOOK_EVENT_TYPES, CURSOR_EVENT_MAP, CURSOR_TOOL_MAP, OPENCODE_HOOK_SCOPES, OPENCODE_HOOK_EVENT_TYPES, PI_HOOK_SCOPES, PI_HOOK_EVENT_TYPES, PI_EVENT_MAP, GEMINI_HOOK_SCOPES, GEMINI_HOOK_EVENT_TYPES, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
|
|
19
|
+
var HOOK_SCOPES, INTEGRATION_TYPES, CODEX_HOOK_SCOPES, CODEX_HOOK_EVENT_TYPES, CODEX_EVENT_MAP, CODEX_TOOL_MAP, COPILOT_HOOK_SCOPES, COPILOT_HOOK_EVENT_TYPES, COPILOT_TOOL_MAP, CURSOR_HOOK_SCOPES, CURSOR_HOOK_EVENT_TYPES, CURSOR_EVENT_MAP, CURSOR_TOOL_MAP, OPENCODE_HOOK_SCOPES, OPENCODE_HOOK_EVENT_TYPES, OPENCODE_TOOL_MAP, OPENCODE_TOOL_INPUT_MAP, PI_HOOK_SCOPES, PI_HOOK_EVENT_TYPES, PI_EVENT_MAP, PI_TOOL_MAP, PI_TOOL_INPUT_MAP, GEMINI_HOOK_SCOPES, GEMINI_HOOK_EVENT_TYPES, GEMINI_EVENT_MAP, GEMINI_TOOL_MAP, HOOK_EVENT_TYPES, FAILPROOFAI_HOOK_MARKER = "__failproofai_hook__";
|
|
20
20
|
var init_types = __esm(() => {
|
|
21
21
|
HOOK_SCOPES = ["user", "project", "local"];
|
|
22
22
|
INTEGRATION_TYPES = ["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"];
|
|
@@ -108,6 +108,30 @@ var init_types = __esm(() => {
|
|
|
108
108
|
"message.updated",
|
|
109
109
|
"permission.ask"
|
|
110
110
|
];
|
|
111
|
+
OPENCODE_TOOL_MAP = {
|
|
112
|
+
bash: "Bash",
|
|
113
|
+
read: "Read",
|
|
114
|
+
write: "Write",
|
|
115
|
+
edit: "Edit",
|
|
116
|
+
apply_patch: "Edit",
|
|
117
|
+
glob: "Glob",
|
|
118
|
+
grep: "Grep",
|
|
119
|
+
list: "LS",
|
|
120
|
+
webfetch: "WebFetch",
|
|
121
|
+
websearch: "WebSearch",
|
|
122
|
+
todowrite: "TodoWrite",
|
|
123
|
+
todoread: "TodoRead"
|
|
124
|
+
};
|
|
125
|
+
OPENCODE_TOOL_INPUT_MAP = {
|
|
126
|
+
Read: { filePath: "file_path" },
|
|
127
|
+
Write: { filePath: "file_path" },
|
|
128
|
+
Edit: {
|
|
129
|
+
filePath: "file_path",
|
|
130
|
+
oldString: "old_string",
|
|
131
|
+
newString: "new_string",
|
|
132
|
+
replaceAll: "replace_all"
|
|
133
|
+
}
|
|
134
|
+
};
|
|
111
135
|
PI_HOOK_SCOPES = ["user", "project"];
|
|
112
136
|
PI_HOOK_EVENT_TYPES = [
|
|
113
137
|
"session_start",
|
|
@@ -127,6 +151,19 @@ var init_types = __esm(() => {
|
|
|
127
151
|
tool_result: "PostToolUse",
|
|
128
152
|
agent_end: "Stop"
|
|
129
153
|
};
|
|
154
|
+
PI_TOOL_MAP = {
|
|
155
|
+
bash: "Bash",
|
|
156
|
+
read: "Read",
|
|
157
|
+
write: "Write",
|
|
158
|
+
edit: "Edit",
|
|
159
|
+
glob: "Glob",
|
|
160
|
+
grep: "Grep"
|
|
161
|
+
};
|
|
162
|
+
PI_TOOL_INPUT_MAP = {
|
|
163
|
+
Read: { path: "file_path" },
|
|
164
|
+
Write: { path: "file_path" },
|
|
165
|
+
Edit: { path: "file_path" }
|
|
166
|
+
};
|
|
130
167
|
GEMINI_HOOK_SCOPES = ["user", "project"];
|
|
131
168
|
GEMINI_HOOK_EVENT_TYPES = [
|
|
132
169
|
"SessionStart",
|
|
@@ -2089,6 +2126,19 @@ You MUST complete the above action NOW. Do NOT ask the user for confirmation —
|
|
|
2089
2126
|
};
|
|
2090
2127
|
}
|
|
2091
2128
|
if (session?.cli === "pi") {
|
|
2129
|
+
if (eventType === "Stop") {
|
|
2130
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}
|
|
2131
|
+
|
|
2132
|
+
You MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
2133
|
+
return {
|
|
2134
|
+
exitCode: 0,
|
|
2135
|
+
stdout: JSON.stringify({ permission: "deny", reason: reasonText }),
|
|
2136
|
+
stderr: "",
|
|
2137
|
+
policyName: policy.name,
|
|
2138
|
+
reason,
|
|
2139
|
+
decision: "deny"
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2092
2142
|
const response = {
|
|
2093
2143
|
permission: "deny",
|
|
2094
2144
|
reason: blockedMessage
|
|
@@ -2267,6 +2317,21 @@ You MUST complete the above action NOW. Do NOT ask the user for confirmation —
|
|
|
2267
2317
|
};
|
|
2268
2318
|
}
|
|
2269
2319
|
if (session?.cli === "pi") {
|
|
2320
|
+
if (eventType === "Stop") {
|
|
2321
|
+
const policyAttribution = policyNames.length === 1 ? `policy: ${policyNames[0]}` : `policies: ${policyNames.join(", ")}`;
|
|
2322
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}
|
|
2323
|
+
|
|
2324
|
+
You MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
2325
|
+
return {
|
|
2326
|
+
exitCode: 0,
|
|
2327
|
+
stdout: JSON.stringify({ permission: "deny", reason: reasonText }),
|
|
2328
|
+
stderr: "",
|
|
2329
|
+
policyName: policyNames[0],
|
|
2330
|
+
policyNames,
|
|
2331
|
+
reason: combined,
|
|
2332
|
+
decision: "instruct"
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2270
2335
|
const response2 = {
|
|
2271
2336
|
permission: "allow",
|
|
2272
2337
|
reason: `Instruction from failproofai: ${combined}`
|
|
@@ -2864,7 +2929,7 @@ var init_hook_activity_store = __esm(() => {
|
|
|
2864
2929
|
});
|
|
2865
2930
|
|
|
2866
2931
|
// package.json
|
|
2867
|
-
var version2 = "0.0.10
|
|
2932
|
+
var version2 = "0.0.10";
|
|
2868
2933
|
var init_package = () => {};
|
|
2869
2934
|
|
|
2870
2935
|
// src/posthog-key.ts
|
|
@@ -3609,7 +3674,6 @@ __export(exports_opencode_projects, {
|
|
|
3609
3674
|
getCachedOpenCodeProjects: () => getCachedOpenCodeProjects
|
|
3610
3675
|
});
|
|
3611
3676
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3612
|
-
import { basename as basename2 } from "node:path";
|
|
3613
3677
|
function runOpenCodeDb(sql) {
|
|
3614
3678
|
try {
|
|
3615
3679
|
const stdout = execFileSync2("opencode", ["db", "--format", "json", sql], {
|
|
@@ -3661,7 +3725,7 @@ async function getOpenCodeProjects() {
|
|
|
3661
3725
|
seen.add(projectId);
|
|
3662
3726
|
const proj = projectMap.get(projectId);
|
|
3663
3727
|
const worktree = proj?.worktree ?? group.rows[0]?.directory ?? null;
|
|
3664
|
-
const name =
|
|
3728
|
+
const name = worktree ? encodeFolderName(worktree) : projectId;
|
|
3665
3729
|
const path = worktree ?? "";
|
|
3666
3730
|
const lastModified = new Date(Math.max(group.latest, proj?.time_updated ?? 0));
|
|
3667
3731
|
out.push({
|
|
@@ -3677,7 +3741,7 @@ async function getOpenCodeProjects() {
|
|
|
3677
3741
|
if (seen.has(p.id))
|
|
3678
3742
|
continue;
|
|
3679
3743
|
const worktree = p.worktree ?? "";
|
|
3680
|
-
const name =
|
|
3744
|
+
const name = worktree ? encodeFolderName(worktree) : p.id;
|
|
3681
3745
|
const lastModified = new Date(p.time_updated);
|
|
3682
3746
|
out.push({
|
|
3683
3747
|
name,
|
|
@@ -3930,7 +3994,7 @@ __export(exports_gemini_projects, {
|
|
|
3930
3994
|
getCachedGeminiSessionsByEncodedName: () => getCachedGeminiSessionsByEncodedName,
|
|
3931
3995
|
getCachedGeminiProjects: () => getCachedGeminiProjects
|
|
3932
3996
|
});
|
|
3933
|
-
import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "node:fs/promises";
|
|
3997
|
+
import { open as open3, readdir as readdir5, readFile as readFile6, stat as stat5 } from "node:fs/promises";
|
|
3934
3998
|
import { homedir as homedir11 } from "node:os";
|
|
3935
3999
|
import { join as join9 } from "node:path";
|
|
3936
4000
|
function getGeminiTmpRoot() {
|
|
@@ -3950,6 +4014,37 @@ async function statMtime4(path) {
|
|
|
3950
4014
|
return null;
|
|
3951
4015
|
}
|
|
3952
4016
|
}
|
|
4017
|
+
async function readFirstLine2(filePath) {
|
|
4018
|
+
let fh = null;
|
|
4019
|
+
try {
|
|
4020
|
+
fh = await open3(filePath, "r");
|
|
4021
|
+
const buf = Buffer.alloc(FIRST_LINE_CHUNK_BYTES2);
|
|
4022
|
+
const { bytesRead } = await fh.read(buf, 0, FIRST_LINE_CHUNK_BYTES2, 0);
|
|
4023
|
+
if (bytesRead === 0)
|
|
4024
|
+
return null;
|
|
4025
|
+
const slice = buf.subarray(0, bytesRead);
|
|
4026
|
+
const nl = slice.indexOf(10);
|
|
4027
|
+
const end = nl === -1 ? bytesRead : nl;
|
|
4028
|
+
return slice.subarray(0, end).toString("utf-8");
|
|
4029
|
+
} catch {
|
|
4030
|
+
return null;
|
|
4031
|
+
} finally {
|
|
4032
|
+
if (fh)
|
|
4033
|
+
await fh.close().catch(() => {});
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
function extractFullSessionId(line) {
|
|
4037
|
+
if (!line)
|
|
4038
|
+
return;
|
|
4039
|
+
try {
|
|
4040
|
+
const meta = JSON.parse(line);
|
|
4041
|
+
if (typeof meta.sessionId !== "string")
|
|
4042
|
+
return;
|
|
4043
|
+
return UUID_RE.test(meta.sessionId) ? meta.sessionId : undefined;
|
|
4044
|
+
} catch {
|
|
4045
|
+
return;
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
3953
4048
|
async function readProjectRoot(projectDir) {
|
|
3954
4049
|
try {
|
|
3955
4050
|
const text = await readFile6(join9(projectDir, ".project_root"), "utf-8");
|
|
@@ -3983,7 +4078,8 @@ async function scanGeminiSessions() {
|
|
|
3983
4078
|
const mtime = await statMtime4(filePath);
|
|
3984
4079
|
if (!mtime)
|
|
3985
4080
|
continue;
|
|
3986
|
-
|
|
4081
|
+
const sessionId = extractFullSessionId(await readFirstLine2(filePath));
|
|
4082
|
+
out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime, sessionId });
|
|
3987
4083
|
}
|
|
3988
4084
|
}), 16);
|
|
3989
4085
|
return out;
|
|
@@ -4011,17 +4107,14 @@ async function getGeminiProjects() {
|
|
|
4011
4107
|
async function getGeminiSessionsForCwd(cwd) {
|
|
4012
4108
|
const sessions = await scanGeminiSessions();
|
|
4013
4109
|
const matches = sessions.filter((s) => s.cwd === cwd);
|
|
4014
|
-
const files = matches.map((s) => {
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
cli: "gemini"
|
|
4023
|
-
};
|
|
4024
|
-
});
|
|
4110
|
+
const files = matches.map((s) => ({
|
|
4111
|
+
name: s.sessionFilename,
|
|
4112
|
+
path: s.filePath,
|
|
4113
|
+
lastModified: s.fileMtime,
|
|
4114
|
+
lastModifiedFormatted: formatDate(s.fileMtime),
|
|
4115
|
+
sessionId: s.sessionId,
|
|
4116
|
+
cli: "gemini"
|
|
4117
|
+
}));
|
|
4025
4118
|
files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
4026
4119
|
return files;
|
|
4027
4120
|
}
|
|
@@ -4038,25 +4131,24 @@ async function getGeminiSessionsByEncodedName(name) {
|
|
|
4038
4131
|
if (uniqueCwds.length !== 1) {
|
|
4039
4132
|
return { cwd: null, sessions: [] };
|
|
4040
4133
|
}
|
|
4041
|
-
const sessions = matches.map((s) => {
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
cli: "gemini"
|
|
4050
|
-
};
|
|
4051
|
-
});
|
|
4134
|
+
const sessions = matches.map((s) => ({
|
|
4135
|
+
name: s.sessionFilename,
|
|
4136
|
+
path: s.filePath,
|
|
4137
|
+
lastModified: s.fileMtime,
|
|
4138
|
+
lastModifiedFormatted: formatDate(s.fileMtime),
|
|
4139
|
+
sessionId: s.sessionId,
|
|
4140
|
+
cli: "gemini"
|
|
4141
|
+
}));
|
|
4052
4142
|
sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
4053
4143
|
return { cwd: uniqueCwds[0], sessions };
|
|
4054
4144
|
}
|
|
4055
|
-
var SESSION_FILE_RE2, getCachedGeminiProjects, getCachedGeminiSessionsByEncodedName;
|
|
4145
|
+
var SESSION_FILE_RE2, UUID_RE, FIRST_LINE_CHUNK_BYTES2, getCachedGeminiProjects, getCachedGeminiSessionsByEncodedName;
|
|
4056
4146
|
var init_gemini_projects = __esm(() => {
|
|
4057
4147
|
init_paths();
|
|
4058
4148
|
init_logger();
|
|
4059
4149
|
SESSION_FILE_RE2 = /^session-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2})-([0-9a-f]{8})\.jsonl$/i;
|
|
4150
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4151
|
+
FIRST_LINE_CHUNK_BYTES2 = 4 * 1024;
|
|
4060
4152
|
getCachedGeminiProjects = runtimeCache(getGeminiProjects, 30);
|
|
4061
4153
|
getCachedGeminiSessionsByEncodedName = runtimeCache((name) => getGeminiSessionsByEncodedName(name), 30, { maxSize: 50 });
|
|
4062
4154
|
});
|
|
@@ -5041,7 +5133,7 @@ var init_copilot_sessions = __esm(() => {
|
|
|
5041
5133
|
// lib/cursor-sessions.ts
|
|
5042
5134
|
import { readFileSync as readFileSync6, readdirSync as readdirSync5, existsSync as existsSync7, statSync as statSync6 } from "node:fs";
|
|
5043
5135
|
import { readFile as readFile10 } from "node:fs/promises";
|
|
5044
|
-
import { basename as
|
|
5136
|
+
import { basename as basename2, join as join15, resolve as resolve7, sep as sep3 } from "node:path";
|
|
5045
5137
|
import { homedir as homedir14 } from "node:os";
|
|
5046
5138
|
function getCursorHome2() {
|
|
5047
5139
|
return process.env.CURSOR_HOME || join15(homedir14(), ".cursor");
|
|
@@ -5079,7 +5171,7 @@ function findCursorTranscript(sessionId) {
|
|
|
5079
5171
|
const dir = getCursorSessionDir(sessionId);
|
|
5080
5172
|
if (!dir)
|
|
5081
5173
|
return null;
|
|
5082
|
-
const newCandidate = join15(dir, `${
|
|
5174
|
+
const newCandidate = join15(dir, `${basename2(dir)}.jsonl`);
|
|
5083
5175
|
if (existsSync7(newCandidate))
|
|
5084
5176
|
return newCandidate;
|
|
5085
5177
|
for (const name of LEGACY_TRANSCRIPT_FILE_CANDIDATES2) {
|
|
@@ -5303,7 +5395,7 @@ function getPiSessionStateRoot() {
|
|
|
5303
5395
|
return process.env.PI_SESSIONS_DIR || join16(homedir15(), ".pi", "agent", "sessions");
|
|
5304
5396
|
}
|
|
5305
5397
|
function isSafeSessionId(sessionId) {
|
|
5306
|
-
return
|
|
5398
|
+
return UUID_RE2.test(sessionId);
|
|
5307
5399
|
}
|
|
5308
5400
|
function findPiTranscript(sessionId) {
|
|
5309
5401
|
if (!isSafeSessionId(sessionId))
|
|
@@ -5475,10 +5567,10 @@ async function getPiSessionLog(sessionId) {
|
|
|
5475
5567
|
filePath
|
|
5476
5568
|
};
|
|
5477
5569
|
}
|
|
5478
|
-
var
|
|
5570
|
+
var UUID_RE2, SESSION_FILE_RE3, getCachedPiSessionLog;
|
|
5479
5571
|
var init_pi_sessions = __esm(() => {
|
|
5480
5572
|
init_log_entries();
|
|
5481
|
-
|
|
5573
|
+
UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5482
5574
|
SESSION_FILE_RE3 = /^[\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;
|
|
5483
5575
|
getCachedPiSessionLog = runtimeCache((sessionId) => getPiSessionLog(sessionId), 60, { maxSize: 50 });
|
|
5484
5576
|
});
|
|
@@ -5492,7 +5584,7 @@ function getGeminiSessionStateRoot() {
|
|
|
5492
5584
|
return process.env.GEMINI_SESSIONS_DIR || join17(homedir16(), ".gemini", "tmp");
|
|
5493
5585
|
}
|
|
5494
5586
|
function isSafeSessionId2(sessionId) {
|
|
5495
|
-
return
|
|
5587
|
+
return UUID_RE3.test(sessionId);
|
|
5496
5588
|
}
|
|
5497
5589
|
function readFirstLineSync(path) {
|
|
5498
5590
|
let fd;
|
|
@@ -5703,10 +5795,10 @@ async function getGeminiSessionLog(sessionId) {
|
|
|
5703
5795
|
filePath
|
|
5704
5796
|
};
|
|
5705
5797
|
}
|
|
5706
|
-
var
|
|
5798
|
+
var UUID_RE3, SESSION_FILE_RE4, getCachedGeminiSessionLog;
|
|
5707
5799
|
var init_gemini_sessions = __esm(() => {
|
|
5708
5800
|
init_log_entries();
|
|
5709
|
-
|
|
5801
|
+
UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5710
5802
|
SESSION_FILE_RE4 = /^session-(.+)-([0-9a-f]{8})\.jsonl$/i;
|
|
5711
5803
|
getCachedGeminiSessionLog = runtimeCache((sessionId) => getGeminiSessionLog(sessionId), 60, { maxSize: 50 });
|
|
5712
5804
|
});
|
|
@@ -6397,8 +6489,28 @@ function canonicalizeToolName(raw, cli) {
|
|
|
6397
6489
|
return CODEX_TOOL_MAP[raw] ?? raw;
|
|
6398
6490
|
if (cli === "gemini")
|
|
6399
6491
|
return GEMINI_TOOL_MAP[raw] ?? raw;
|
|
6492
|
+
if (cli === "opencode")
|
|
6493
|
+
return OPENCODE_TOOL_MAP[raw] ?? raw;
|
|
6494
|
+
if (cli === "pi")
|
|
6495
|
+
return PI_TOOL_MAP[raw] ?? raw;
|
|
6400
6496
|
return raw;
|
|
6401
6497
|
}
|
|
6498
|
+
function canonicalizeToolInput(toolName, rawInput, cli) {
|
|
6499
|
+
if (!toolName || !rawInput || typeof rawInput !== "object")
|
|
6500
|
+
return rawInput;
|
|
6501
|
+
let perToolMap;
|
|
6502
|
+
if (cli === "opencode")
|
|
6503
|
+
perToolMap = OPENCODE_TOOL_INPUT_MAP[toolName];
|
|
6504
|
+
else if (cli === "pi")
|
|
6505
|
+
perToolMap = PI_TOOL_INPUT_MAP[toolName];
|
|
6506
|
+
if (!perToolMap)
|
|
6507
|
+
return rawInput;
|
|
6508
|
+
const out = {};
|
|
6509
|
+
for (const [k, v] of Object.entries(rawInput)) {
|
|
6510
|
+
out[perToolMap[k] ?? k] = v;
|
|
6511
|
+
}
|
|
6512
|
+
return out;
|
|
6513
|
+
}
|
|
6402
6514
|
async function handleHookEvent(eventType, cli = "claude") {
|
|
6403
6515
|
const startTime = performance.now();
|
|
6404
6516
|
const MAX_STDIN_BYTES = 1048576;
|
|
@@ -6440,6 +6552,11 @@ async function handleHookEvent(eventType, cli = "claude") {
|
|
|
6440
6552
|
if (canonicalToolName !== rawToolName) {
|
|
6441
6553
|
parsed.tool_name = canonicalToolName;
|
|
6442
6554
|
}
|
|
6555
|
+
const rawInput = parsed.tool_input;
|
|
6556
|
+
const canonicalInput = canonicalizeToolInput(canonicalToolName, rawInput, cli);
|
|
6557
|
+
if (canonicalInput !== rawInput) {
|
|
6558
|
+
parsed.tool_input = canonicalInput;
|
|
6559
|
+
}
|
|
6443
6560
|
const sessionId = parsed.session_id;
|
|
6444
6561
|
const session = {
|
|
6445
6562
|
sessionId,
|
|
@@ -6955,6 +7072,28 @@ function canonicalizeTool(raw) {
|
|
|
6955
7072
|
return TOOL_NAME_MAP[raw] != null ? TOOL_NAME_MAP[raw] : raw;
|
|
6956
7073
|
}
|
|
6957
7074
|
|
|
7075
|
+
// Per-tool input-key translation: opencode native tools deliver args as
|
|
7076
|
+
// camelCase (\`filePath\`, \`oldString\`, …) but failproofai builtin policies
|
|
7077
|
+
// (\`block-read-outside-cwd\`, \`block-env-files\`, \`block-secrets-write\`)
|
|
7078
|
+
// read \`ctx.toolInput.file_path\` etc. Without this map every Read/Write/Edit
|
|
7079
|
+
// path-check silently no-ops on opencode. Keys are PascalCase canonical tool
|
|
7080
|
+
// names so the lookup pairs with canonicalizeTool's output. Tools outside the
|
|
7081
|
+
// map (MCP \`mcp_*\`, plugins) pass through unchanged. Keep in sync with
|
|
7082
|
+
// OPENCODE_TOOL_INPUT_MAP in failproofai/src/hooks/types.ts.
|
|
7083
|
+
const TOOL_INPUT_MAP = {
|
|
7084
|
+
Read: { filePath: "file_path" },
|
|
7085
|
+
Write: { filePath: "file_path" },
|
|
7086
|
+
Edit: { filePath: "file_path", oldString: "old_string", newString: "new_string", replaceAll: "replace_all" },
|
|
7087
|
+
};
|
|
7088
|
+
function canonicalizeToolInput(canonicalToolName, args) {
|
|
7089
|
+
if (!args || typeof args !== "object") return args;
|
|
7090
|
+
const map = TOOL_INPUT_MAP[canonicalToolName];
|
|
7091
|
+
if (!map) return args;
|
|
7092
|
+
const out = {};
|
|
7093
|
+
for (const k of Object.keys(args)) out[map[k] != null ? map[k] : k] = args[k];
|
|
7094
|
+
return out;
|
|
7095
|
+
}
|
|
7096
|
+
|
|
6958
7097
|
const FAILPROOFAI_BIN = ${escapedBin};
|
|
6959
7098
|
const USE_NPX = ${useNpx};
|
|
6960
7099
|
|
|
@@ -7053,11 +7192,12 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
7053
7192
|
|
|
7054
7193
|
// First-class PreToolUse hook. Note: tool args live on output.args (mutable).
|
|
7055
7194
|
"tool.execute.before": async (input, output) => {
|
|
7195
|
+
const canonicalTool = canonicalizeTool(input.tool);
|
|
7056
7196
|
const r = runFailproofai("PreToolUse", {
|
|
7057
7197
|
session_id: input.sessionID,
|
|
7058
7198
|
cwd: directory,
|
|
7059
|
-
tool_name:
|
|
7060
|
-
tool_input: output.args,
|
|
7199
|
+
tool_name: canonicalTool,
|
|
7200
|
+
tool_input: canonicalizeToolInput(canonicalTool, output.args),
|
|
7061
7201
|
hook_event_name: "PreToolUse",
|
|
7062
7202
|
}, directory);
|
|
7063
7203
|
await applyDecision(r, { client, sessionID: input.sessionID }, "PreToolUse");
|
|
@@ -7065,11 +7205,12 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
7065
7205
|
|
|
7066
7206
|
// First-class PostToolUse hook. Note: tool args live on input.args here.
|
|
7067
7207
|
"tool.execute.after": async (input, output) => {
|
|
7208
|
+
const canonicalTool = canonicalizeTool(input.tool);
|
|
7068
7209
|
const r = runFailproofai("PostToolUse", {
|
|
7069
7210
|
session_id: input.sessionID,
|
|
7070
7211
|
cwd: directory,
|
|
7071
|
-
tool_name:
|
|
7072
|
-
tool_input: input.args,
|
|
7212
|
+
tool_name: canonicalTool,
|
|
7213
|
+
tool_input: canonicalizeToolInput(canonicalTool, input.args),
|
|
7073
7214
|
tool_response: { title: output.title, output: output.output, metadata: output.metadata },
|
|
7074
7215
|
hook_event_name: "PostToolUse",
|
|
7075
7216
|
}, directory);
|
|
@@ -7078,11 +7219,12 @@ export default async function failproofaiPlugin({ client, directory }) {
|
|
|
7078
7219
|
|
|
7079
7220
|
// Cleaner deny UX for prompted tools — mutate output.status instead of throwing.
|
|
7080
7221
|
"permission.ask": async (input, output) => {
|
|
7222
|
+
const canonicalTool = canonicalizeTool(input.tool);
|
|
7081
7223
|
const r = runFailproofai("PermissionRequest", {
|
|
7082
7224
|
session_id: input.sessionID,
|
|
7083
7225
|
cwd: directory,
|
|
7084
|
-
tool_name:
|
|
7085
|
-
tool_input: input,
|
|
7226
|
+
tool_name: canonicalTool || input.command || "permission",
|
|
7227
|
+
tool_input: canonicalizeToolInput(canonicalTool, input),
|
|
7086
7228
|
hook_event_name: "PermissionRequest",
|
|
7087
7229
|
}, directory);
|
|
7088
7230
|
try {
|
|
@@ -8153,7 +8295,7 @@ __export(exports_manager, {
|
|
|
8153
8295
|
});
|
|
8154
8296
|
import { execSync as execSync4 } from "node:child_process";
|
|
8155
8297
|
import { existsSync as existsSync16 } from "node:fs";
|
|
8156
|
-
import { resolve as resolve11, basename as
|
|
8298
|
+
import { resolve as resolve11, basename as basename3 } from "node:path";
|
|
8157
8299
|
import { homedir as homedir23, platform, arch, release, hostname } from "node:os";
|
|
8158
8300
|
function getSettingsPath(scope, cwd) {
|
|
8159
8301
|
return claudeCode.getSettingsPath(scope, cwd);
|
|
@@ -8584,15 +8726,15 @@ Failproof AI Hook Policies
|
|
|
8584
8726
|
try {
|
|
8585
8727
|
const hooks = await loadCustomHooks(file);
|
|
8586
8728
|
if (hooks.length === 0) {
|
|
8587
|
-
const filename =
|
|
8729
|
+
const filename = basename3(file);
|
|
8588
8730
|
console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31mfailed to load\x1B[0m`);
|
|
8589
8731
|
} else {
|
|
8590
|
-
const filename =
|
|
8732
|
+
const filename = basename3(file);
|
|
8591
8733
|
const hookSummary = hooks.map((h) => h.name).join(", ");
|
|
8592
8734
|
console.log(` \x1B[32m✓\x1B[0m ${filename.padEnd(nameColWidth)}${hooks.length} hook(s): ${hookSummary}`);
|
|
8593
8735
|
}
|
|
8594
8736
|
} catch {
|
|
8595
|
-
const filename =
|
|
8737
|
+
const filename = basename3(file);
|
|
8596
8738
|
console.log(` \x1B[31m✗\x1B[0m ${filename.padEnd(nameColWidth)}\x1B[31merror\x1B[0m`);
|
|
8597
8739
|
}
|
|
8598
8740
|
}
|
|
@@ -9470,27 +9612,10 @@ import { resolve as resolve14, dirname as dirname8 } from "node:path";
|
|
|
9470
9612
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9471
9613
|
function launch(mode) {
|
|
9472
9614
|
const { loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2));
|
|
9473
|
-
const bannerLines = [
|
|
9474
|
-
" ███ ▐███ ▐█",
|
|
9475
|
-
" ▐█▛▀▀ ▟█▖ ▟█▛▀▀ ▝▀",
|
|
9476
|
-
" ██████ ▗████▌ ▝██▛ ██ ███ ▐██▙ ▗███ ▐██▙ ▗█████▙ ████▌ ▐█",
|
|
9477
|
-
" ▀▜█▛▀▀ ▝▀▀▀▀█▙ ▄▙ ██ ▗▟▀▀▜▄▖ ▄▟▀▀▘ ▄▟▛▀▜▙▖ ▄█▀▀█▄▖▝▀██▛▀▀ ▀▀▀▀▙▄ ▐█",
|
|
9478
|
-
" ▐█▌ ▗██████ ███ ██ ▐█ ▐█▌ ██ ██▌ ▐█▌ ██ ██▌ ██▌ ██████ ▐█",
|
|
9479
|
-
" ▐█▌ ▐█▛▀▀██ ██▀ ██ ▐█ ▐█▌ ██ ██▌ ▐█▌ ██ ██▌ ██▌ █▛▀▀██ ▐█",
|
|
9480
|
-
" ▐█▌ ▝▀█████ ██▄▄██ ▐████▀▘ ██ ▀▜███▀▘ ▀▜███▀▘ ██▌ ▀█████ ▐█",
|
|
9481
|
-
" ▝▀▘ ▀▀▀▀▀ ▀▀▀▀▀▀ ▐█▀▀▀ ▀▀ ▝▀▀▀ ▝▀▀▀ ▀▀▘ ▀▀▀▀▀ ▝▀",
|
|
9482
|
-
" ▐█",
|
|
9483
|
-
" ▝▀"
|
|
9484
|
-
];
|
|
9485
|
-
const bannerWidth = bannerLines.reduce((w, l) => Math.max(w, l.length), 0);
|
|
9486
|
-
const cols = process.stdout.columns;
|
|
9487
|
-
const banner = cols !== undefined && cols < bannerWidth ? " failproof ai" : bannerLines.join(`
|
|
9488
|
-
`);
|
|
9489
9615
|
console.log(`
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
v${version2}
|
|
9616
|
+
failproof ai
|
|
9493
9617
|
`);
|
|
9618
|
+
console.log(` \uD83D\uDCE6 Version: ${version2}`);
|
|
9494
9619
|
console.log(` ⭐ Star us: https://github.com/exospherehost/failproofai`);
|
|
9495
9620
|
console.log(` \uD83D\uDCD6 Docs: https://befailproof.ai`);
|
|
9496
9621
|
console.log(` \uD83D\uDCAC Slack: https://join.slack.com/t/failproofai/shared_invite/zt-3v63b7k5e-O3NBHmj8X6n9gZSGDx6ggQ
|
|
@@ -9583,7 +9708,7 @@ import { realpathSync as realpathSync3 } from "fs";
|
|
|
9583
9708
|
import { dirname as dirname9, resolve as resolve15 } from "path";
|
|
9584
9709
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9585
9710
|
// package.json
|
|
9586
|
-
var version = "0.0.10
|
|
9711
|
+
var version = "0.0.10";
|
|
9587
9712
|
|
|
9588
9713
|
// bin/failproofai.mjs
|
|
9589
9714
|
if (!process.env.FAILPROOFAI_PACKAGE_ROOT) {
|
package/lib/gemini-projects.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* missing `~/.gemini/` returns `[]`, malformed JSONL falls open without
|
|
22
22
|
* surfacing the session.
|
|
23
23
|
*/
|
|
24
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
24
|
+
import { open, readdir, readFile, stat } from "node:fs/promises";
|
|
25
25
|
import { homedir } from "node:os";
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
import type { ProjectFolder, SessionFile } from "./projects";
|
|
@@ -34,6 +34,13 @@ import { logWarn } from "./logger";
|
|
|
34
34
|
/** Filename pattern for a Gemini session JSONL:
|
|
35
35
|
* `session-<ISO-timestamp-with-dashes>-<8-hex-uuid-prefix>.jsonl`. */
|
|
36
36
|
const SESSION_FILE_RE = /^session-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2})-([0-9a-f]{8})\.jsonl$/i;
|
|
37
|
+
/** Full UUID — the filename only embeds the first 8 hex chars; the rest is on
|
|
38
|
+
* the JSONL metadata header line. The session detail route requires a full
|
|
39
|
+
* UUID, so links built from the truncated filename prefix 404. */
|
|
40
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
41
|
+
/** Metadata header sits on line 1 and is well under 1 KB; 4 KB covers it
|
|
42
|
+
* comfortably without slurping a multi-MB transcript. */
|
|
43
|
+
const FIRST_LINE_CHUNK_BYTES = 4 * 1024;
|
|
37
44
|
|
|
38
45
|
/** Override for tests. Defaults to the live Gemini session-state root. */
|
|
39
46
|
function getGeminiTmpRoot(): string {
|
|
@@ -46,6 +53,10 @@ interface GeminiSessionMeta {
|
|
|
46
53
|
sessionFilename: string;
|
|
47
54
|
cwd: string;
|
|
48
55
|
fileMtime: Date;
|
|
56
|
+
/** Full UUID parsed from the JSONL metadata header (line 1). Undefined when
|
|
57
|
+
* the header is missing, malformed, or carries a non-UUID `sessionId`;
|
|
58
|
+
* callers fall through to rendering an un-linked row. */
|
|
59
|
+
sessionId?: string;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
async function safeReaddir(dir: string) {
|
|
@@ -64,6 +75,40 @@ async function statMtime(path: string): Promise<Date | null> {
|
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
/** Read the first newline-delimited line of `filePath` without slurping the
|
|
79
|
+
* rest. Mirrors `lib/codex-projects.ts`'s readFirstLine; the Gemini metadata
|
|
80
|
+
* header is always on line 1. */
|
|
81
|
+
async function readFirstLine(filePath: string): Promise<string | null> {
|
|
82
|
+
let fh: Awaited<ReturnType<typeof open>> | null = null;
|
|
83
|
+
try {
|
|
84
|
+
fh = await open(filePath, "r");
|
|
85
|
+
const buf = Buffer.alloc(FIRST_LINE_CHUNK_BYTES);
|
|
86
|
+
const { bytesRead } = await fh.read(buf, 0, FIRST_LINE_CHUNK_BYTES, 0);
|
|
87
|
+
if (bytesRead === 0) return null;
|
|
88
|
+
const slice = buf.subarray(0, bytesRead);
|
|
89
|
+
const nl = slice.indexOf(0x0a); // '\n'
|
|
90
|
+
const end = nl === -1 ? bytesRead : nl;
|
|
91
|
+
return slice.subarray(0, end).toString("utf-8");
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
} finally {
|
|
95
|
+
if (fh) await fh.close().catch(() => {});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Extract a full-UUID `sessionId` from a JSONL metadata header line. Returns
|
|
100
|
+
* undefined on parse failure, missing field, or a non-UUID value. */
|
|
101
|
+
function extractFullSessionId(line: string | null): string | undefined {
|
|
102
|
+
if (!line) return undefined;
|
|
103
|
+
try {
|
|
104
|
+
const meta = JSON.parse(line) as { sessionId?: unknown };
|
|
105
|
+
if (typeof meta.sessionId !== "string") return undefined;
|
|
106
|
+
return UUID_RE.test(meta.sessionId) ? meta.sessionId : undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
67
112
|
/** Read `.project_root` to recover the absolute cwd for a basename folder.
|
|
68
113
|
* Returns null if missing or empty (caller treats the folder as un-mappable). */
|
|
69
114
|
async function readProjectRoot(projectDir: string): Promise<string | null> {
|
|
@@ -101,7 +146,8 @@ async function scanGeminiSessions(): Promise<GeminiSessionMeta[]> {
|
|
|
101
146
|
const filePath = join(chatsDir, f.name);
|
|
102
147
|
const mtime = await statMtime(filePath);
|
|
103
148
|
if (!mtime) continue;
|
|
104
|
-
|
|
149
|
+
const sessionId = extractFullSessionId(await readFirstLine(filePath));
|
|
150
|
+
out.push({ filePath, sessionFilename: f.name, cwd, fileMtime: mtime, sessionId });
|
|
105
151
|
}
|
|
106
152
|
}),
|
|
107
153
|
16,
|
|
@@ -140,17 +186,14 @@ export async function getGeminiProjects(): Promise<ProjectFolder[]> {
|
|
|
140
186
|
export async function getGeminiSessionsForCwd(cwd: string): Promise<SessionFile[]> {
|
|
141
187
|
const sessions = await scanGeminiSessions();
|
|
142
188
|
const matches = sessions.filter((s) => s.cwd === cwd);
|
|
143
|
-
const files: SessionFile[] = matches.map((s) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
cli: "gemini",
|
|
152
|
-
};
|
|
153
|
-
});
|
|
189
|
+
const files: SessionFile[] = matches.map((s) => ({
|
|
190
|
+
name: s.sessionFilename,
|
|
191
|
+
path: s.filePath,
|
|
192
|
+
lastModified: s.fileMtime,
|
|
193
|
+
lastModifiedFormatted: formatDate(s.fileMtime),
|
|
194
|
+
sessionId: s.sessionId,
|
|
195
|
+
cli: "gemini",
|
|
196
|
+
}));
|
|
154
197
|
files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
155
198
|
return files;
|
|
156
199
|
}
|
|
@@ -180,17 +223,14 @@ export async function getGeminiSessionsByEncodedName(name: string): Promise<Gemi
|
|
|
180
223
|
if (uniqueCwds.length !== 1) {
|
|
181
224
|
return { cwd: null, sessions: [] };
|
|
182
225
|
}
|
|
183
|
-
const sessions = matches.map((s) => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
cli: "gemini" as const,
|
|
192
|
-
};
|
|
193
|
-
});
|
|
226
|
+
const sessions = matches.map((s) => ({
|
|
227
|
+
name: s.sessionFilename,
|
|
228
|
+
path: s.filePath,
|
|
229
|
+
lastModified: s.fileMtime,
|
|
230
|
+
lastModifiedFormatted: formatDate(s.fileMtime),
|
|
231
|
+
sessionId: s.sessionId,
|
|
232
|
+
cli: "gemini" as const,
|
|
233
|
+
}));
|
|
194
234
|
sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
195
235
|
return { cwd: uniqueCwds[0], sessions };
|
|
196
236
|
}
|
package/lib/opencode-projects.ts
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
* https://opencode.ai/docs/plugins/ (plugin model context)
|
|
23
23
|
*/
|
|
24
24
|
import { execFileSync } from "node:child_process";
|
|
25
|
-
import { basename } from "node:path";
|
|
26
25
|
import { encodeFolderName } from "./paths";
|
|
27
26
|
import type { ProjectFolder, SessionFile } from "./projects";
|
|
28
27
|
import { runtimeCache } from "./runtime-cache";
|
|
@@ -91,10 +90,11 @@ function readProjectRows(): OpenCodeProjectRow[] | null {
|
|
|
91
90
|
|
|
92
91
|
/**
|
|
93
92
|
* Group sessions by `project_id` and produce one ProjectFolder per project.
|
|
94
|
-
* The folder name
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
93
|
+
* The folder name is `encodeFolderName(worktree)` (matches every other CLI's
|
|
94
|
+
* URL-slug encoding so the dashboard's `/project/[name]` route resolves), or
|
|
95
|
+
* the project_id when no worktree is recorded. `lastModified` is the max
|
|
96
|
+
* session `time_updated` for that project (or the project's own time_updated
|
|
97
|
+
* if no sessions exist yet).
|
|
98
98
|
*/
|
|
99
99
|
export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {
|
|
100
100
|
const sessions = readSessionRows();
|
|
@@ -122,13 +122,15 @@ export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {
|
|
|
122
122
|
|
|
123
123
|
// Emit one ProjectFolder per project that has at least one session OR a
|
|
124
124
|
// project row (covers projects opencode knows about but hasn't run yet).
|
|
125
|
+
// `name` is the dashboard's URL slug — must be `encodeFolderName(cwd)` to
|
|
126
|
+
// match every other CLI (and the resolver in `getOpenCodeSessionsByEncodedName`).
|
|
125
127
|
const seen = new Set<string>();
|
|
126
128
|
const out: ProjectFolder[] = [];
|
|
127
129
|
for (const [projectId, group] of groups) {
|
|
128
130
|
seen.add(projectId);
|
|
129
131
|
const proj = projectMap.get(projectId);
|
|
130
132
|
const worktree = proj?.worktree ?? group.rows[0]?.directory ?? null;
|
|
131
|
-
const name =
|
|
133
|
+
const name = worktree ? encodeFolderName(worktree) : projectId;
|
|
132
134
|
const path = worktree ?? "";
|
|
133
135
|
const lastModified = new Date(Math.max(group.latest, proj?.time_updated ?? 0));
|
|
134
136
|
out.push({
|
|
@@ -143,7 +145,7 @@ export async function getOpenCodeProjects(): Promise<ProjectFolder[]> {
|
|
|
143
145
|
for (const p of projects ?? []) {
|
|
144
146
|
if (seen.has(p.id)) continue;
|
|
145
147
|
const worktree = p.worktree ?? "";
|
|
146
|
-
const name =
|
|
148
|
+
const name = worktree ? encodeFolderName(worktree) : p.id;
|
|
147
149
|
const lastModified = new Date(p.time_updated);
|
|
148
150
|
out.push({
|
|
149
151
|
name,
|