failproofai 0.0.9-beta.2 → 0.0.10-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.cursor/hooks.json +47 -0
- package/.next/standalone/.gemini/settings.json +147 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.~nmr9._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0yspgjy._.js → [root-of-the-server]__010i6f5._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08px0ym._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kjo7d_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wu7fr7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z4c5dj._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zso~62._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09icjsf._.js → [root-of-the-server]__0709m8.._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0vrlxf2._.js → [root-of-the-server]__0ymn496._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0eu4j_n._.js → [root-of-the-server]__10h.ggz._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_07a1g.3._.js → _0zx~s__._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_copilot-projects_ts_0r8xkn8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_cursor-projects_ts_0qt1scg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_pi-projects_ts_103tsh1._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/static/chunks/{06j6c0ofqjy0v.js → 00ay03h8bq4b~.js} +2 -2
- package/.next/standalone/.next/static/chunks/{15_mi91qaeieu.js → 0agmlhk5ml7x5.js} +1 -1
- package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
- package/.next/standalone/.next/static/chunks/0en4v5k2nnxks.js +1 -0
- package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0zdn~84f58hvf.js → 0s6nux54y~l~r.js} +1 -1
- package/.next/standalone/.next/static/chunks/{10mlwc4y_kqo2.js → 0tpse0wu2wwo0.js} +1 -1
- package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
- package/.next/standalone/.next/static/chunks/{0e8c_1f7-8e7t.js → 1400rtd5ywbt..js} +2 -2
- package/.next/standalone/.next/static/chunks/{0mumk7h5i.1xd.js → 14lmf8boay-zu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{18a9xv2p3~x.9.js → 17htukxga7bil.js} +1 -1
- package/.next/standalone/.opencode/opencode.json +4 -0
- package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
- package/.next/standalone/.pi/settings.json +5 -0
- package/.next/standalone/app/components/cli-badge.tsx +7 -11
- package/.next/standalone/app/components/project-list.tsx +32 -4
- package/.next/standalone/app/policies/hooks-client.tsx +31 -15
- package/.next/standalone/app/project/[name]/page.tsx +52 -16
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
- package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
- package/.next/standalone/assets/logos/copilot-light.svg +1 -0
- package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
- package/.next/standalone/assets/logos/cursor-light.svg +1 -0
- package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
- package/.next/standalone/assets/logos/gemini-light.svg +13 -0
- package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
- package/.next/standalone/assets/logos/opencode-light.svg +1 -0
- package/.next/standalone/assets/logos/pi-dark.svg +7 -0
- package/.next/standalone/assets/logos/pi-light.svg +7 -0
- package/.next/standalone/lib/cli-registry.ts +107 -0
- package/.next/standalone/lib/codex-projects.ts +3 -3
- package/.next/standalone/lib/copilot-projects.ts +224 -0
- package/.next/standalone/lib/copilot-sessions.ts +395 -0
- package/.next/standalone/lib/cursor-projects.ts +312 -0
- package/.next/standalone/lib/cursor-sessions.ts +467 -0
- package/.next/standalone/lib/gemini-projects.ts +203 -0
- package/.next/standalone/lib/gemini-sessions.ts +365 -0
- package/.next/standalone/lib/opencode-projects.ts +232 -0
- package/.next/standalone/lib/opencode-sessions.ts +237 -0
- package/.next/standalone/lib/pi-projects.ts +230 -0
- package/.next/standalone/lib/pi-sessions.ts +325 -0
- package/.next/standalone/lib/projects.ts +67 -31
- package/.next/standalone/next.config.ts +5 -4
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/pi-extension/index.ts +373 -0
- package/.next/standalone/pi-extension/package.json +12 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +37 -3
- package/bin/failproofai.mjs +61 -21
- package/dist/cli.mjs +2248 -246
- package/lib/cli-registry.ts +107 -0
- package/lib/codex-projects.ts +3 -3
- package/lib/copilot-projects.ts +224 -0
- package/lib/copilot-sessions.ts +395 -0
- package/lib/cursor-projects.ts +312 -0
- package/lib/cursor-sessions.ts +467 -0
- package/lib/gemini-projects.ts +203 -0
- package/lib/gemini-sessions.ts +365 -0
- package/lib/opencode-projects.ts +232 -0
- package/lib/opencode-sessions.ts +237 -0
- package/lib/pi-projects.ts +230 -0
- package/lib/pi-sessions.ts +325 -0
- package/lib/projects.ts +67 -31
- package/package.json +2 -1
- package/pi-extension/index.ts +373 -0
- package/pi-extension/package.json +12 -0
- package/scripts/translate-docs/mdx-translator.ts +56 -2
- package/scripts/translate-docs/translator.ts +1 -1
- package/src/hooks/builtin-policies.ts +84 -14
- package/src/hooks/handler.ts +67 -5
- package/src/hooks/install-prompt.ts +33 -10
- package/src/hooks/integrations.ts +1007 -6
- package/src/hooks/policy-evaluator.ts +299 -3
- package/src/hooks/resolve-permission-mode.ts +23 -0
- package/src/hooks/types.ts +307 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
- package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
- package/.next/standalone/.next/static/chunks/03egp37o1l629.js +0 -1
- package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
- package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
- /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 68TLSFdjAQYIulNHfP0QY}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 68TLSFdjAQYIulNHfP0QY}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{SyaO-J1hupjAiRCG-Syzg → 68TLSFdjAQYIulNHfP0QY}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode (sst/opencode) session transcript loader.
|
|
3
|
+
*
|
|
4
|
+
* Sessions live in opencode's SQLite DB (`~/.local/share/opencode/opencode.db`),
|
|
5
|
+
* not on disk as JSONL like the other CLIs. We read them by shelling out to
|
|
6
|
+
* `opencode db --format json "<sql>"` — same surface as `lib/opencode-projects.ts`
|
|
7
|
+
* and the same fail-open contract (binary missing → return null).
|
|
8
|
+
*
|
|
9
|
+
* Schema verified live on opencode v1.14.31:
|
|
10
|
+
* • `session(id, project_id, parent_id, slug, directory, title, time_*, …)`
|
|
11
|
+
* • `message(id, session_id, time_created, time_updated, data: JSON)`
|
|
12
|
+
* • `part(id, message_id, session_id, time_created, time_updated, data: JSON)`
|
|
13
|
+
*
|
|
14
|
+
* The `data` column on message/part is an opaque JSON blob; we parse it
|
|
15
|
+
* defensively (degrade unknown types to system entries) so a future opencode
|
|
16
|
+
* release that adds new shapes doesn't break the dashboard.
|
|
17
|
+
*
|
|
18
|
+
* Refs: https://opencode.ai/docs/ (CLI reference)
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { runtimeCache } from "./runtime-cache";
|
|
22
|
+
import {
|
|
23
|
+
baseEntry,
|
|
24
|
+
type LogEntry,
|
|
25
|
+
type UserEntry,
|
|
26
|
+
type AssistantEntry,
|
|
27
|
+
type GenericEntry,
|
|
28
|
+
type ContentBlock,
|
|
29
|
+
type LogSource,
|
|
30
|
+
} from "./log-entries";
|
|
31
|
+
|
|
32
|
+
interface OpenCodeSessionRow {
|
|
33
|
+
id: string;
|
|
34
|
+
project_id: string;
|
|
35
|
+
slug: string | null;
|
|
36
|
+
directory: string | null;
|
|
37
|
+
title: string | null;
|
|
38
|
+
time_created: number;
|
|
39
|
+
time_updated: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface OpenCodeMessageRow {
|
|
43
|
+
id: string;
|
|
44
|
+
session_id: string;
|
|
45
|
+
time_created: number;
|
|
46
|
+
time_updated: number;
|
|
47
|
+
data: string; // JSON-encoded
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface OpenCodePartRow {
|
|
51
|
+
id: string;
|
|
52
|
+
message_id: string;
|
|
53
|
+
session_id: string;
|
|
54
|
+
time_created: number;
|
|
55
|
+
time_updated: number;
|
|
56
|
+
data: string; // JSON-encoded
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Run a parameter-free SELECT against opencode's DB. Returns `null` on any
|
|
60
|
+
* failure (binary missing, query error, malformed output). */
|
|
61
|
+
function runOpenCodeDb<T>(sql: string): T[] | null {
|
|
62
|
+
try {
|
|
63
|
+
const stdout = execFileSync("opencode", ["db", "--format", "json", sql], {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
timeout: 5_000,
|
|
66
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
67
|
+
});
|
|
68
|
+
if (!stdout.trim()) return [];
|
|
69
|
+
const parsed = JSON.parse(stdout) as unknown;
|
|
70
|
+
if (!Array.isArray(parsed)) return null;
|
|
71
|
+
return parsed as T[];
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
78
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Safely JSON.parse a string column from a DB row. */
|
|
82
|
+
function parseDataColumn(raw: string | undefined | null): Record<string, unknown> | null {
|
|
83
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
86
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Read content text safely — opencode parts may carry text under `text` or
|
|
93
|
+
* `content` (depending on part type). Anything non-string degrades to "". */
|
|
94
|
+
function readContentText(data: Record<string, unknown>): string {
|
|
95
|
+
if (typeof data.text === "string") return data.text;
|
|
96
|
+
if (typeof data.content === "string") return data.content;
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Translate a (message, parts[]) tuple into a single LogEntry. */
|
|
101
|
+
function translateMessage(
|
|
102
|
+
msgRow: OpenCodeMessageRow,
|
|
103
|
+
partRows: OpenCodePartRow[],
|
|
104
|
+
source: LogSource,
|
|
105
|
+
): LogEntry {
|
|
106
|
+
const msgData = parseDataColumn(msgRow.data) ?? {};
|
|
107
|
+
const role = typeof msgData.role === "string" ? msgData.role : "system";
|
|
108
|
+
const date = new Date(msgRow.time_created);
|
|
109
|
+
const timestamp = date.toISOString();
|
|
110
|
+
const raw: Record<string, unknown> = { uuid: msgRow.id, parentUuid: null };
|
|
111
|
+
const base = baseEntry(raw, timestamp, date, source);
|
|
112
|
+
|
|
113
|
+
// Build content blocks from parts. opencode part types we recognize:
|
|
114
|
+
// • text → text block
|
|
115
|
+
// • tool → tool_use envelope (tool name + input args)
|
|
116
|
+
// • everything else → preserve as a text block with a debug tag
|
|
117
|
+
const content: ContentBlock[] = [];
|
|
118
|
+
let userText = "";
|
|
119
|
+
for (const p of partRows) {
|
|
120
|
+
const data = parseDataColumn(p.data);
|
|
121
|
+
if (!data) continue;
|
|
122
|
+
const type = typeof data.type === "string" ? data.type : "unknown";
|
|
123
|
+
if (type === "text") {
|
|
124
|
+
const text = readContentText(data);
|
|
125
|
+
if (text) {
|
|
126
|
+
content.push({ type: "text", text });
|
|
127
|
+
userText += (userText ? "\n" : "") + text;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (type === "tool") {
|
|
132
|
+
const toolName = typeof data.tool === "string" ? data.tool : (typeof data.name === "string" ? data.name : "tool");
|
|
133
|
+
const input = isPlainObject(data.input) ? data.input : (isPlainObject(data.args) ? data.args : {});
|
|
134
|
+
content.push({
|
|
135
|
+
type: "tool_use",
|
|
136
|
+
id: p.id,
|
|
137
|
+
name: toolName,
|
|
138
|
+
input: input as Record<string, unknown>,
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Unknown part type — preserve as a text annotation rather than drop silently.
|
|
143
|
+
content.push({ type: "text", text: `[opencode ${type}]` });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (role === "user") {
|
|
147
|
+
const entry: UserEntry = {
|
|
148
|
+
...base,
|
|
149
|
+
type: "user",
|
|
150
|
+
message: { role: "user", content: userText },
|
|
151
|
+
};
|
|
152
|
+
return entry;
|
|
153
|
+
}
|
|
154
|
+
if (role === "assistant") {
|
|
155
|
+
const modelInfo = isPlainObject(msgData.model) ? msgData.model : null;
|
|
156
|
+
const modelStr = modelInfo && typeof modelInfo.modelID === "string" ? modelInfo.modelID : undefined;
|
|
157
|
+
const entry: AssistantEntry = {
|
|
158
|
+
...base,
|
|
159
|
+
type: "assistant",
|
|
160
|
+
message: { role: "assistant", content, model: modelStr },
|
|
161
|
+
};
|
|
162
|
+
return entry;
|
|
163
|
+
}
|
|
164
|
+
// Fallback — system / unknown roles surface as generic entries so nothing is lost.
|
|
165
|
+
const entry: GenericEntry = {
|
|
166
|
+
...base,
|
|
167
|
+
type: "system",
|
|
168
|
+
raw: { id: msgRow.id, role, parts: content },
|
|
169
|
+
};
|
|
170
|
+
return entry;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface OpenCodeSessionLogData {
|
|
174
|
+
entries: LogEntry[];
|
|
175
|
+
rawLines: Record<string, unknown>[];
|
|
176
|
+
cwd?: string;
|
|
177
|
+
filePath: string; // synthetic — opencode doesn't have a file path; we use opencode://<id>
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Load a single session by ID. Returns null when the session doesn't exist
|
|
182
|
+
* or the binary is unavailable.
|
|
183
|
+
*/
|
|
184
|
+
export async function getOpenCodeSessionLog(sessionId: string): Promise<OpenCodeSessionLogData | null> {
|
|
185
|
+
if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return null; // SQL-injection guard
|
|
186
|
+
const sessions = runOpenCodeDb<OpenCodeSessionRow>(
|
|
187
|
+
`SELECT id, project_id, slug, directory, title, time_created, time_updated FROM session WHERE id = '${sessionId}'`,
|
|
188
|
+
);
|
|
189
|
+
if (!sessions || sessions.length === 0) return null;
|
|
190
|
+
const session = sessions[0];
|
|
191
|
+
|
|
192
|
+
const messages = runOpenCodeDb<OpenCodeMessageRow>(
|
|
193
|
+
`SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = '${sessionId}' ORDER BY time_created ASC`,
|
|
194
|
+
);
|
|
195
|
+
const parts = runOpenCodeDb<OpenCodePartRow>(
|
|
196
|
+
`SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = '${sessionId}' ORDER BY time_created ASC`,
|
|
197
|
+
);
|
|
198
|
+
if (!messages) return { entries: [], rawLines: [], cwd: session.directory ?? undefined, filePath: `opencode://${sessionId}` };
|
|
199
|
+
|
|
200
|
+
// Group parts by message_id for O(1) lookup.
|
|
201
|
+
const partsByMessage = new Map<string, OpenCodePartRow[]>();
|
|
202
|
+
for (const p of parts ?? []) {
|
|
203
|
+
let bucket = partsByMessage.get(p.message_id);
|
|
204
|
+
if (!bucket) {
|
|
205
|
+
bucket = [];
|
|
206
|
+
partsByMessage.set(p.message_id, bucket);
|
|
207
|
+
}
|
|
208
|
+
bucket.push(p);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const entries: LogEntry[] = [];
|
|
212
|
+
const rawLines: Record<string, unknown>[] = [];
|
|
213
|
+
for (const msg of messages) {
|
|
214
|
+
const partRows = partsByMessage.get(msg.id) ?? [];
|
|
215
|
+
entries.push(translateMessage(msg, partRows, "session"));
|
|
216
|
+
const data = parseDataColumn(msg.data);
|
|
217
|
+
rawLines.push({
|
|
218
|
+
id: msg.id,
|
|
219
|
+
session_id: msg.session_id,
|
|
220
|
+
time_created: msg.time_created,
|
|
221
|
+
data: data ?? msg.data,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
entries,
|
|
227
|
+
rawLines,
|
|
228
|
+
cwd: session.directory ?? undefined,
|
|
229
|
+
filePath: `opencode://${sessionId}`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const getCachedOpenCodeSessionLog = runtimeCache(
|
|
234
|
+
(sessionId: string) => getOpenCodeSessionLog(sessionId),
|
|
235
|
+
30,
|
|
236
|
+
{ maxSize: 50 },
|
|
237
|
+
);
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi (pi-coding-agent) project discovery.
|
|
3
|
+
*
|
|
4
|
+
* Empirically verified against pi-coding-agent v0.72.1 (Phase 0.7 of plan):
|
|
5
|
+
*
|
|
6
|
+
* • Session-state root: `~/.pi/agent/sessions/` (NOT `~/.pi/sessions/`).
|
|
7
|
+
* • Per-cwd subdirs are encoded with `/` → `-` and wrapped in `--…--`,
|
|
8
|
+
* e.g. `/home/u/repo` → `--home-u-repo--`. The encoding is LOSSY
|
|
9
|
+
* (literal `-` in the path is preserved as `-` and indistinguishable
|
|
10
|
+
* from a path separator), so we never use the dir name as the canonical
|
|
11
|
+
* cwd — we read the first JSONL record (`{type: "session", cwd, ...}`)
|
|
12
|
+
* to recover it exactly.
|
|
13
|
+
* • Per-session file: `<ISO-timestamp>_<UUID>.jsonl`, e.g.
|
|
14
|
+
* `2026-05-01T20-36-22-628Z_019de541-b7e3-7131-abac-d15df780042c.jsonl`.
|
|
15
|
+
* • File format: JSONL. First record always shape
|
|
16
|
+
* `{type: "session", version, id, timestamp, cwd}`.
|
|
17
|
+
*
|
|
18
|
+
* As with Cursor/Copilot, this module is intentionally permissive — a
|
|
19
|
+
* missing `~/.pi/` returns `[]`, and a malformed JSONL falls open without
|
|
20
|
+
* surfacing the session.
|
|
21
|
+
*/
|
|
22
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { encodeFolderName } from "./paths";
|
|
26
|
+
import type { ProjectFolder, SessionFile } from "./projects";
|
|
27
|
+
import { runtimeCache } from "./runtime-cache";
|
|
28
|
+
import { batchAll } from "./concurrency";
|
|
29
|
+
import { formatDate } from "./format-date";
|
|
30
|
+
import { logWarn } from "./logger";
|
|
31
|
+
|
|
32
|
+
/** Filename pattern for a Pi session JSONL: `<iso-timestamp>_<uuid>.jsonl`. */
|
|
33
|
+
const SESSION_FILE_RE = /^[\d-]+T[\d-]+Z_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
34
|
+
|
|
35
|
+
function getPiSessionsRoot(): string {
|
|
36
|
+
return process.env.PI_SESSIONS_DIR
|
|
37
|
+
|| join(homedir(), ".pi", "agent", "sessions");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface PiSessionMeta {
|
|
41
|
+
filePath: string;
|
|
42
|
+
sessionId: string;
|
|
43
|
+
cwd: string;
|
|
44
|
+
fileMtime: Date;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function safeReaddir(dir: string) {
|
|
48
|
+
try {
|
|
49
|
+
return await readdir(dir, { withFileTypes: true });
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function statMtime(path: string): Promise<Date | null> {
|
|
56
|
+
try {
|
|
57
|
+
return (await stat(path)).mtime;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Reads the first newline-terminated record of a Pi JSONL file and returns
|
|
64
|
+
* its `cwd` field. Returns null on read/parse failure or when the first
|
|
65
|
+
* record isn't `{type: "session"}`. */
|
|
66
|
+
async function readSessionCwd(filePath: string): Promise<string | null> {
|
|
67
|
+
let text: string;
|
|
68
|
+
try {
|
|
69
|
+
text = await readFile(filePath, "utf-8");
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const firstLine = text.indexOf("\n") >= 0 ? text.slice(0, text.indexOf("\n")) : text;
|
|
74
|
+
if (!firstLine) return null;
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(firstLine) as { type?: unknown; cwd?: unknown };
|
|
77
|
+
if (parsed.type !== "session") return null;
|
|
78
|
+
if (typeof parsed.cwd !== "string" || parsed.cwd.length === 0) return null;
|
|
79
|
+
return parsed.cwd;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function scanPiSessions(): Promise<PiSessionMeta[]> {
|
|
86
|
+
const root = getPiSessionsRoot();
|
|
87
|
+
const cwdDirs = await safeReaddir(root);
|
|
88
|
+
if (!cwdDirs) return [];
|
|
89
|
+
|
|
90
|
+
const candidates: { sessionId: string; filePath: string }[] = [];
|
|
91
|
+
for (const cwdDir of cwdDirs) {
|
|
92
|
+
if (!cwdDir.isDirectory()) continue;
|
|
93
|
+
const cwdPath = join(root, cwdDir.name);
|
|
94
|
+
const sessionFiles = await safeReaddir(cwdPath);
|
|
95
|
+
if (!sessionFiles) continue;
|
|
96
|
+
for (const f of sessionFiles) {
|
|
97
|
+
if (!f.isFile()) continue;
|
|
98
|
+
const m = SESSION_FILE_RE.exec(f.name);
|
|
99
|
+
if (!m) continue;
|
|
100
|
+
candidates.push({ sessionId: m[1], filePath: join(cwdPath, f.name) });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (candidates.length === 0) return [];
|
|
104
|
+
|
|
105
|
+
const settled = await batchAll(
|
|
106
|
+
candidates.map((c) => async (): Promise<PiSessionMeta | null> => {
|
|
107
|
+
const cwd = await readSessionCwd(c.filePath);
|
|
108
|
+
if (!cwd) return null;
|
|
109
|
+
const mtime = await statMtime(c.filePath);
|
|
110
|
+
if (!mtime) return null;
|
|
111
|
+
return {
|
|
112
|
+
filePath: c.filePath,
|
|
113
|
+
sessionId: c.sessionId,
|
|
114
|
+
cwd,
|
|
115
|
+
fileMtime: mtime,
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
16,
|
|
119
|
+
);
|
|
120
|
+
return settled
|
|
121
|
+
.filter((r): r is PromiseFulfilledResult<PiSessionMeta | null> => r.status === "fulfilled")
|
|
122
|
+
.map((r) => r.value)
|
|
123
|
+
.filter((v): v is PiSessionMeta => v !== null);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const cachedScan = runtimeCache(scanPiSessions, 30);
|
|
127
|
+
|
|
128
|
+
/** Returns one ProjectFolder per unique cwd discovered in Pi transcripts. */
|
|
129
|
+
export async function getPiProjects(): Promise<ProjectFolder[]> {
|
|
130
|
+
let metas: PiSessionMeta[];
|
|
131
|
+
try {
|
|
132
|
+
metas = await cachedScan();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logWarn("Failed to scan Pi sessions:", error);
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const byCwd = new Map<string, { latest: Date; cwd: string }>();
|
|
139
|
+
for (const m of metas) {
|
|
140
|
+
const existing = byCwd.get(m.cwd);
|
|
141
|
+
if (!existing || m.fileMtime.getTime() > existing.latest.getTime()) {
|
|
142
|
+
byCwd.set(m.cwd, { latest: m.fileMtime, cwd: m.cwd });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const folders: ProjectFolder[] = [];
|
|
147
|
+
for (const { cwd, latest } of byCwd.values()) {
|
|
148
|
+
folders.push({
|
|
149
|
+
name: encodeFolderName(cwd),
|
|
150
|
+
path: cwd,
|
|
151
|
+
isDirectory: true,
|
|
152
|
+
lastModified: latest,
|
|
153
|
+
lastModifiedFormatted: formatDate(latest),
|
|
154
|
+
cli: ["pi"],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
158
|
+
return folders;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function metasToSessionFiles(metas: PiSessionMeta[]): SessionFile[] {
|
|
162
|
+
const files: SessionFile[] = metas.map((m) => ({
|
|
163
|
+
name: m.sessionId,
|
|
164
|
+
path: m.filePath,
|
|
165
|
+
lastModified: m.fileMtime,
|
|
166
|
+
lastModifiedFormatted: formatDate(m.fileMtime),
|
|
167
|
+
sessionId: m.sessionId,
|
|
168
|
+
cli: "pi",
|
|
169
|
+
}));
|
|
170
|
+
files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Returns SessionFile entries for every Pi transcript whose cwd matches `cwd` exactly. */
|
|
175
|
+
export async function getPiSessionsForCwd(cwd: string): Promise<SessionFile[]> {
|
|
176
|
+
let metas: PiSessionMeta[];
|
|
177
|
+
try {
|
|
178
|
+
metas = await cachedScan();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
logWarn("Failed to scan Pi sessions:", error);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
return metasToSessionFiles(metas.filter((m) => m.cwd === cwd));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface PiProjectByName {
|
|
187
|
+
cwd: string | null;
|
|
188
|
+
sessions: SessionFile[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Looks up Pi sessions for a project URL slug. `decodeFolderName` is lossy on
|
|
193
|
+
* cwds containing `-`, so we re-encode each session's cwd via
|
|
194
|
+
* `encodeFolderName` and match in that direction. Returns both the canonical
|
|
195
|
+
* cwd and the matching sessions.
|
|
196
|
+
*/
|
|
197
|
+
export async function getPiSessionsByEncodedName(name: string): Promise<PiProjectByName> {
|
|
198
|
+
let metas: PiSessionMeta[];
|
|
199
|
+
try {
|
|
200
|
+
metas = await cachedScan();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
logWarn("Failed to scan Pi sessions:", error);
|
|
203
|
+
return { cwd: null, sessions: [] };
|
|
204
|
+
}
|
|
205
|
+
const matches = metas.filter((m) => encodeFolderName(m.cwd) === name);
|
|
206
|
+
// encodeFolderName is lossy — distinct cwd values can collapse to the same
|
|
207
|
+
// encoded slug. Only return a canonical cwd when every match agrees;
|
|
208
|
+
// otherwise drop the cwd and the (mixed) sessions to avoid surfacing the
|
|
209
|
+
// wrong project label.
|
|
210
|
+
const uniqueCwds = Array.from(new Set(matches.map((m) => m.cwd)));
|
|
211
|
+
if (uniqueCwds.length !== 1) {
|
|
212
|
+
return { cwd: null, sessions: [] };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
cwd: uniqueCwds[0],
|
|
216
|
+
sessions: metasToSessionFiles(matches),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const getCachedPiProjects = runtimeCache(getPiProjects, 30);
|
|
221
|
+
export const getCachedPiSessionsForCwd = runtimeCache(
|
|
222
|
+
(cwd: string) => getPiSessionsForCwd(cwd),
|
|
223
|
+
30,
|
|
224
|
+
{ maxSize: 50 },
|
|
225
|
+
);
|
|
226
|
+
export const getCachedPiSessionsByEncodedName = runtimeCache(
|
|
227
|
+
(name: string) => getPiSessionsByEncodedName(name),
|
|
228
|
+
30,
|
|
229
|
+
{ maxSize: 50 },
|
|
230
|
+
);
|