failproofai 0.0.9 → 0.0.10-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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]__0_b7pgn._.js → [root-of-the-server]__0ymn496._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.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/{0n-_j_6fo6jex.js → 00ay03h8bq4b~.js} +2 -2
- package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.js → 0agmlhk5ml7x5.js} +1 -1
- package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
- package/.next/standalone/.next/static/chunks/{095l4hc7-h.~~.js → 0en4v5k2nnxks.js} +1 -1
- package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0s6nux54y~l~r.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.js → 0tpse0wu2wwo0.js} +1 -1
- package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
- package/.next/standalone/.next/static/chunks/{0u-ys71jc4y68.js → 1400rtd5ywbt..js} +2 -2
- package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 14lmf8boay-zu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.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/06x4-d1~o-opr.js +0 -1
- package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot CLI project discovery.
|
|
3
|
+
*
|
|
4
|
+
* Copilot stores per-session state at `~/.copilot/session-state/<sessionId>/`,
|
|
5
|
+
* with a `workspace.yaml` carrying flat scalars: id, cwd, git_root, branch,
|
|
6
|
+
* repository, host_type, user_named, summary_count, created_at, updated_at,
|
|
7
|
+
* (optional) name, summary. We read this file (always present, even before
|
|
8
|
+
* any interaction creates events.jsonl) to extract the cwd. Sessions are
|
|
9
|
+
* grouped by unique cwd into `ProjectFolder` rows.
|
|
10
|
+
*
|
|
11
|
+
* The encoded cwd doubles as the URL slug for `/project/[name]`, matching the
|
|
12
|
+
* Claude Code convention (see `encodeFolderName` in `lib/paths.ts`), so a cwd
|
|
13
|
+
* present in multiple stores naturally produces the same `name` and merges in
|
|
14
|
+
* `lib/projects.ts`.
|
|
15
|
+
*/
|
|
16
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { encodeFolderName } from "./paths";
|
|
20
|
+
import type { ProjectFolder, SessionFile } from "./projects";
|
|
21
|
+
import { runtimeCache } from "./runtime-cache";
|
|
22
|
+
import { batchAll } from "./concurrency";
|
|
23
|
+
import { formatDate } from "./format-date";
|
|
24
|
+
import { logWarn } from "./logger";
|
|
25
|
+
|
|
26
|
+
/** Inlined to avoid cross-module imports from `lib/copilot-sessions.ts` —
|
|
27
|
+
* keeping the dep tree independent prevents Turbopack from tracing
|
|
28
|
+
* Node-only modules (`fs/promises`, `os`) into the client bundle when the
|
|
29
|
+
* session viewer page statically imports `copilot-sessions`. Mirrors the
|
|
30
|
+
* pattern in `lib/codex-projects.ts`. */
|
|
31
|
+
function getCopilotSessionStateRoot(): string {
|
|
32
|
+
return join(process.env.COPILOT_HOME || join(homedir(), ".copilot"), "session-state");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface CopilotSessionMeta {
|
|
36
|
+
workspacePath: string;
|
|
37
|
+
eventsPath: string;
|
|
38
|
+
sessionId: string;
|
|
39
|
+
cwd: string;
|
|
40
|
+
/** Latest of (workspace.yaml mtime, events.jsonl mtime if present). */
|
|
41
|
+
fileMtime: Date;
|
|
42
|
+
/** True iff `events.jsonl` exists. Workspace-only sessions (initialized but
|
|
43
|
+
* never sent a prompt) skip the `/project` session list because the viewer
|
|
44
|
+
* would only render "Session log file not found." */
|
|
45
|
+
hasTranscript: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function safeReaddir(dir: string) {
|
|
49
|
+
try {
|
|
50
|
+
return await readdir(dir, { withFileTypes: true });
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Extract `cwd` from a workspace.yaml file. Permissive regex parser — avoids
|
|
57
|
+
* pulling in a YAML lib for one flat scalar. Copilot writes simple
|
|
58
|
+
* `key: value` lines without nesting in this file (verified against CLI 1.0.39). */
|
|
59
|
+
function parseCwdFromWorkspace(text: string): string | undefined {
|
|
60
|
+
const m = text.match(/^cwd\s*:\s*(.+?)\s*$/m);
|
|
61
|
+
if (!m) return undefined;
|
|
62
|
+
return m[1].replace(/^['"]|['"]$/g, "");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function statMtime(path: string): Promise<Date | null> {
|
|
66
|
+
try {
|
|
67
|
+
return (await stat(path)).mtime;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function scanCopilotSessions(): Promise<CopilotSessionMeta[]> {
|
|
74
|
+
const root = getCopilotSessionStateRoot();
|
|
75
|
+
const entries = await safeReaddir(root);
|
|
76
|
+
if (!entries) return [];
|
|
77
|
+
|
|
78
|
+
const candidates = entries
|
|
79
|
+
.filter((e) => e.isDirectory())
|
|
80
|
+
.map((e) => ({
|
|
81
|
+
sessionId: e.name,
|
|
82
|
+
workspacePath: join(root, e.name, "workspace.yaml"),
|
|
83
|
+
eventsPath: join(root, e.name, "events.jsonl"),
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
const settled = await batchAll(
|
|
87
|
+
candidates.map((c) => async (): Promise<CopilotSessionMeta | null> => {
|
|
88
|
+
let workspaceText: string;
|
|
89
|
+
try {
|
|
90
|
+
workspaceText = await readFile(c.workspacePath, "utf-8");
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const cwd = parseCwdFromWorkspace(workspaceText);
|
|
95
|
+
if (!cwd) return null;
|
|
96
|
+
// Prefer events.jsonl mtime when present (reflects last activity);
|
|
97
|
+
// fall back to workspace.yaml mtime for sessions without interaction.
|
|
98
|
+
const eventsMtime = await statMtime(c.eventsPath);
|
|
99
|
+
const wsMtime = await statMtime(c.workspacePath);
|
|
100
|
+
const hasTranscript = eventsMtime !== null;
|
|
101
|
+
const fileMtime =
|
|
102
|
+
eventsMtime && wsMtime
|
|
103
|
+
? new Date(Math.max(eventsMtime.getTime(), wsMtime.getTime()))
|
|
104
|
+
: eventsMtime ?? wsMtime ?? new Date(0);
|
|
105
|
+
return {
|
|
106
|
+
workspacePath: c.workspacePath,
|
|
107
|
+
eventsPath: c.eventsPath,
|
|
108
|
+
sessionId: c.sessionId,
|
|
109
|
+
cwd,
|
|
110
|
+
fileMtime,
|
|
111
|
+
hasTranscript,
|
|
112
|
+
};
|
|
113
|
+
}),
|
|
114
|
+
16,
|
|
115
|
+
);
|
|
116
|
+
return settled
|
|
117
|
+
.filter((r): r is PromiseFulfilledResult<CopilotSessionMeta | null> => r.status === "fulfilled")
|
|
118
|
+
.map((r) => r.value)
|
|
119
|
+
.filter((v): v is CopilotSessionMeta => v !== null);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cachedScan = runtimeCache(scanCopilotSessions, 30);
|
|
123
|
+
|
|
124
|
+
/** Returns one ProjectFolder per unique cwd discovered in Copilot transcripts. */
|
|
125
|
+
export async function getCopilotProjects(): Promise<ProjectFolder[]> {
|
|
126
|
+
let metas: CopilotSessionMeta[];
|
|
127
|
+
try {
|
|
128
|
+
metas = await cachedScan();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logWarn("Failed to scan Copilot sessions:", error);
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Skip workspace-only sessions: their /projects rows would click through to
|
|
135
|
+
// an empty session list (metasToSessionFiles also filters on hasTranscript).
|
|
136
|
+
const byCwd = new Map<string, { latest: Date; cwd: string }>();
|
|
137
|
+
for (const m of metas) {
|
|
138
|
+
if (!m.hasTranscript) continue;
|
|
139
|
+
const existing = byCwd.get(m.cwd);
|
|
140
|
+
if (!existing || m.fileMtime.getTime() > existing.latest.getTime()) {
|
|
141
|
+
byCwd.set(m.cwd, { latest: m.fileMtime, cwd: m.cwd });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const folders: ProjectFolder[] = [];
|
|
146
|
+
for (const { cwd, latest } of byCwd.values()) {
|
|
147
|
+
folders.push({
|
|
148
|
+
name: encodeFolderName(cwd),
|
|
149
|
+
path: cwd,
|
|
150
|
+
isDirectory: true,
|
|
151
|
+
lastModified: latest,
|
|
152
|
+
lastModifiedFormatted: formatDate(latest),
|
|
153
|
+
cli: ["copilot"],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
157
|
+
return folders;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function metasToSessionFiles(metas: CopilotSessionMeta[]): SessionFile[] {
|
|
161
|
+
// Skip workspace-only sessions: their "openable" rows would lead to a
|
|
162
|
+
// 'Session log file not found' viewer because events.jsonl doesn't exist.
|
|
163
|
+
const files: SessionFile[] = metas
|
|
164
|
+
.filter((m) => m.hasTranscript)
|
|
165
|
+
.map((m) => ({
|
|
166
|
+
name: m.sessionId,
|
|
167
|
+
path: m.eventsPath,
|
|
168
|
+
lastModified: m.fileMtime,
|
|
169
|
+
lastModifiedFormatted: formatDate(m.fileMtime),
|
|
170
|
+
sessionId: m.sessionId,
|
|
171
|
+
cli: "copilot",
|
|
172
|
+
}));
|
|
173
|
+
files.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
174
|
+
return files;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Returns SessionFile entries for every Copilot transcript whose cwd matches `cwd` exactly. */
|
|
178
|
+
export async function getCopilotSessionsForCwd(cwd: string): Promise<SessionFile[]> {
|
|
179
|
+
let metas: CopilotSessionMeta[];
|
|
180
|
+
try {
|
|
181
|
+
metas = await cachedScan();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
logWarn("Failed to scan Copilot sessions:", error);
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
return metasToSessionFiles(metas.filter((m) => m.cwd === cwd));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface CopilotProjectByName {
|
|
190
|
+
cwd: string | null;
|
|
191
|
+
sessions: SessionFile[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Looks up Copilot sessions for a project URL slug. `decodeFolderName` is lossy
|
|
196
|
+
* (every `-` becomes `/`), so we re-encode each session's cwd and match in
|
|
197
|
+
* that direction. Returns both the canonical cwd and the matching sessions.
|
|
198
|
+
*/
|
|
199
|
+
export async function getCopilotSessionsByEncodedName(name: string): Promise<CopilotProjectByName> {
|
|
200
|
+
let metas: CopilotSessionMeta[];
|
|
201
|
+
try {
|
|
202
|
+
metas = await cachedScan();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logWarn("Failed to scan Copilot sessions:", error);
|
|
205
|
+
return { cwd: null, sessions: [] };
|
|
206
|
+
}
|
|
207
|
+
const matches = metas.filter((m) => m.hasTranscript && encodeFolderName(m.cwd) === name);
|
|
208
|
+
return {
|
|
209
|
+
cwd: matches[0]?.cwd ?? null,
|
|
210
|
+
sessions: metasToSessionFiles(matches),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const getCachedCopilotProjects = runtimeCache(getCopilotProjects, 30);
|
|
215
|
+
export const getCachedCopilotSessionsForCwd = runtimeCache(
|
|
216
|
+
(cwd: string) => getCopilotSessionsForCwd(cwd),
|
|
217
|
+
30,
|
|
218
|
+
{ maxSize: 50 },
|
|
219
|
+
);
|
|
220
|
+
export const getCachedCopilotSessionsByEncodedName = runtimeCache(
|
|
221
|
+
(name: string) => getCopilotSessionsByEncodedName(name),
|
|
222
|
+
30,
|
|
223
|
+
{ maxSize: 50 },
|
|
224
|
+
);
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Copilot CLI session transcript discovery + JSONL parser.
|
|
3
|
+
*
|
|
4
|
+
* Copilot stores per-session state at:
|
|
5
|
+
* ~/.copilot/session-state/<sessionId>/
|
|
6
|
+
* workspace.yaml — session metadata (id, cwd, git_root, branch, …)
|
|
7
|
+
* events.jsonl — event log (only created after first interaction)
|
|
8
|
+
* session.db — per-session SQLite (cross-session index lives at ~/.copilot/session-store.db)
|
|
9
|
+
* checkpoints/index.md — checkpoint history
|
|
10
|
+
* files/, research/ — workspace artifacts
|
|
11
|
+
*
|
|
12
|
+
* (configurable via COPILOT_HOME). Each `events.jsonl` line is a record with
|
|
13
|
+
* shape `{ type, data, id, timestamp, parentId }` where `type` is a dotted
|
|
14
|
+
* path. Verified record types as of Copilot CLI 1.0.39:
|
|
15
|
+
* • session.start — data.sessionId, data.context.{cwd, gitRoot, branch, repository, headCommit}
|
|
16
|
+
* • session.model_change — data.newModel
|
|
17
|
+
* • session.shutdown — data.shutdownType, data.codeChanges, …
|
|
18
|
+
* • system.message — data.role, data.content
|
|
19
|
+
* • user.message — data.content, data.transformedContent
|
|
20
|
+
* • assistant.turn_start — data.turnId, data.interactionId
|
|
21
|
+
* • assistant.message — data.messageId, data.content, data.toolRequests
|
|
22
|
+
* • assistant.turn_end — data.turnId
|
|
23
|
+
* • tool.execution_start — data.toolCallId, data.toolName, data.arguments
|
|
24
|
+
* • tool.execution_complete — data.toolCallId, data.success, data.result.{content, detailedContent}
|
|
25
|
+
*
|
|
26
|
+
* Unknown record types are preserved as generic system entries so nothing is
|
|
27
|
+
* silently dropped.
|
|
28
|
+
*
|
|
29
|
+
* Refs:
|
|
30
|
+
* https://docs.github.com/en/copilot/concepts/agents/copilot-cli/chronicle
|
|
31
|
+
* https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
|
|
32
|
+
*/
|
|
33
|
+
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
34
|
+
import { readFile } from "node:fs/promises";
|
|
35
|
+
import { join, resolve, sep } from "node:path";
|
|
36
|
+
import { homedir } from "node:os";
|
|
37
|
+
import { runtimeCache } from "./runtime-cache";
|
|
38
|
+
import {
|
|
39
|
+
baseEntry,
|
|
40
|
+
formatTimestamp,
|
|
41
|
+
type LogEntry,
|
|
42
|
+
type UserEntry,
|
|
43
|
+
type AssistantEntry,
|
|
44
|
+
type GenericEntry,
|
|
45
|
+
type QueueOperationEntry,
|
|
46
|
+
type ContentBlock,
|
|
47
|
+
type ToolUseBlock,
|
|
48
|
+
type LogSource,
|
|
49
|
+
} from "./log-entries";
|
|
50
|
+
import { formatDuration } from "./format-duration";
|
|
51
|
+
|
|
52
|
+
// ── Paths ──
|
|
53
|
+
|
|
54
|
+
/** Root directory for Copilot CLI session state, honoring COPILOT_HOME. */
|
|
55
|
+
export function getCopilotHome(): string {
|
|
56
|
+
return process.env.COPILOT_HOME || join(homedir(), ".copilot");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getCopilotSessionStateRoot(): string {
|
|
60
|
+
return join(getCopilotHome(), "session-state");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Session-state dir for a given sessionId. Returns null if `sessionId` is
|
|
64
|
+
* empty or contains path-traversal segments that would escape the
|
|
65
|
+
* session-state root. */
|
|
66
|
+
export function getCopilotSessionDir(sessionId: string): string | null {
|
|
67
|
+
if (!sessionId) return null;
|
|
68
|
+
const root = resolve(getCopilotSessionStateRoot());
|
|
69
|
+
const candidate = resolve(root, sessionId);
|
|
70
|
+
// Containment check: the resolved path must be a child of `root`.
|
|
71
|
+
// (Equality means `sessionId` resolved to root itself — also rejected.)
|
|
72
|
+
if (candidate === root || !candidate.startsWith(`${root}${sep}`)) return null;
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Resolve a single file under a session directory, applying the same
|
|
77
|
+
* containment check. Returns null if the sessionId is invalid. */
|
|
78
|
+
function resolveSessionFile(sessionId: string, filename: string): string | null {
|
|
79
|
+
const dir = getCopilotSessionDir(sessionId);
|
|
80
|
+
if (!dir) return null;
|
|
81
|
+
return join(dir, filename);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Transcript discovery ──
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Locate a Copilot CLI events.jsonl by sessionId. Copilot lays sessions out
|
|
88
|
+
* directly under `session-state/<sessionId>/events.jsonl` (only created after
|
|
89
|
+
* the first user interaction), so the lookup is a single existence check.
|
|
90
|
+
* Path-traversal sessionIds (`../foo`) are rejected.
|
|
91
|
+
*
|
|
92
|
+
* Synchronous so the hook hot path can call it without awaits.
|
|
93
|
+
*/
|
|
94
|
+
export function findCopilotTranscript(sessionId: string): string | null {
|
|
95
|
+
const candidate = resolveSessionFile(sessionId, "events.jsonl");
|
|
96
|
+
if (!candidate) return null;
|
|
97
|
+
return existsSync(candidate) ? candidate : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Locate the workspace.yaml for a session (always present, even pre-interaction). */
|
|
101
|
+
export function findCopilotWorkspace(sessionId: string): string | null {
|
|
102
|
+
const candidate = resolveSessionFile(sessionId, "workspace.yaml");
|
|
103
|
+
if (!candidate) return null;
|
|
104
|
+
return existsSync(candidate) ? candidate : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract a single key from the YAML at `path` using a permissive regex
|
|
109
|
+
* (avoids adding a YAML parser dep for the handful of flat scalar fields
|
|
110
|
+
* Copilot writes). Returns the trimmed string value or undefined.
|
|
111
|
+
*/
|
|
112
|
+
function readYamlScalar(path: string, key: string): string | undefined {
|
|
113
|
+
try {
|
|
114
|
+
const text = readFileSync(path, "utf-8");
|
|
115
|
+
const re = new RegExp(`^${key}\\s*:\\s*(.+?)\\s*$`, "m");
|
|
116
|
+
const m = text.match(re);
|
|
117
|
+
if (!m) return undefined;
|
|
118
|
+
// Strip surrounding quotes if any.
|
|
119
|
+
return m[1].replace(/^['"]|['"]$/g, "");
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Read the cwd recorded in workspace.yaml for a session. */
|
|
126
|
+
export function readCopilotWorkspaceCwd(sessionId: string): string | undefined {
|
|
127
|
+
const path = findCopilotWorkspace(sessionId);
|
|
128
|
+
if (!path) return undefined;
|
|
129
|
+
return readYamlScalar(path, "cwd");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Parser ──
|
|
133
|
+
|
|
134
|
+
interface CopilotRecord {
|
|
135
|
+
type?: string;
|
|
136
|
+
data?: Record<string, unknown>;
|
|
137
|
+
id?: string;
|
|
138
|
+
timestamp?: string;
|
|
139
|
+
parentId?: string | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface CopilotParseResult {
|
|
143
|
+
entries: LogEntry[];
|
|
144
|
+
rawLines: Record<string, unknown>[];
|
|
145
|
+
/** Working directory pulled from the first session.start record. */
|
|
146
|
+
cwd?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface CopilotToolResult {
|
|
150
|
+
content?: string;
|
|
151
|
+
detailedContent?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface CopilotToolTelemetry {
|
|
155
|
+
metrics?: { commandTimeMs?: number; durationMs?: number };
|
|
156
|
+
properties?: Record<string, unknown>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse a Copilot CLI events.jsonl transcript into `LogEntry[]` plus the raw
|
|
161
|
+
* lines. Yields to the event loop every 200 lines so big transcripts don't
|
|
162
|
+
* block the request.
|
|
163
|
+
*/
|
|
164
|
+
export async function parseCopilotLog(
|
|
165
|
+
fileContent: string,
|
|
166
|
+
source: LogSource = "session",
|
|
167
|
+
): Promise<CopilotParseResult> {
|
|
168
|
+
const lines = fileContent.split("\n").filter((line) => line.trim() !== "");
|
|
169
|
+
const entries: LogEntry[] = [];
|
|
170
|
+
const rawLines: Record<string, unknown>[] = [];
|
|
171
|
+
// toolCallId → tool_use block, so we can attach tool.execution_complete back.
|
|
172
|
+
const toolUseById = new Map<string, ToolUseBlock>();
|
|
173
|
+
const toolUseStartMs = new Map<string, number>();
|
|
174
|
+
let cwd: string | undefined;
|
|
175
|
+
let seenSessionStart = false;
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
178
|
+
if (i > 0 && i % 200 === 0) await new Promise<void>((r) => setImmediate(r));
|
|
179
|
+
|
|
180
|
+
const line = lines[i];
|
|
181
|
+
let raw: CopilotRecord;
|
|
182
|
+
try {
|
|
183
|
+
raw = JSON.parse(line) as CopilotRecord;
|
|
184
|
+
} catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rawCopy = { ...(raw as Record<string, unknown>), _source: source };
|
|
189
|
+
rawLines.push(rawCopy);
|
|
190
|
+
|
|
191
|
+
const timestampStr = raw.timestamp;
|
|
192
|
+
if (!timestampStr) continue;
|
|
193
|
+
const date = new Date(timestampStr);
|
|
194
|
+
if (Number.isNaN(date.getTime())) continue;
|
|
195
|
+
const timestamp = date.toISOString();
|
|
196
|
+
|
|
197
|
+
const recType = raw.type;
|
|
198
|
+
const data = raw.data ?? {};
|
|
199
|
+
|
|
200
|
+
if (recType === "session.start") {
|
|
201
|
+
const ctx = data.context as { cwd?: unknown } | undefined;
|
|
202
|
+
const c = ctx?.cwd;
|
|
203
|
+
if (typeof c === "string" && !cwd) cwd = c;
|
|
204
|
+
const label: QueueOperationEntry["label"] = seenSessionStart ? "Session Resumed" : "Session Started";
|
|
205
|
+
seenSessionStart = true;
|
|
206
|
+
entries.push({
|
|
207
|
+
type: "queue-operation",
|
|
208
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
209
|
+
label,
|
|
210
|
+
} satisfies QueueOperationEntry);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (recType === "user.message") {
|
|
215
|
+
const text = (data.content as string) ?? "";
|
|
216
|
+
if (!text) continue;
|
|
217
|
+
entries.push({
|
|
218
|
+
type: "user",
|
|
219
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
220
|
+
message: { role: "user", content: text },
|
|
221
|
+
} satisfies UserEntry);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (recType === "system.message") {
|
|
226
|
+
// System prompts are noisy; render as a generic system entry so they're
|
|
227
|
+
// visible in the raw view but don't dominate the structured timeline.
|
|
228
|
+
entries.push({
|
|
229
|
+
type: "system",
|
|
230
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
231
|
+
raw: rawCopy,
|
|
232
|
+
} satisfies GenericEntry);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (recType === "assistant.message") {
|
|
237
|
+
const text = (data.content as string) ?? "";
|
|
238
|
+
if (!text) {
|
|
239
|
+
entries.push({
|
|
240
|
+
type: "system",
|
|
241
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
242
|
+
raw: rawCopy,
|
|
243
|
+
} satisfies GenericEntry);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const blocks: ContentBlock[] = [{ type: "text", text }];
|
|
247
|
+
entries.push({
|
|
248
|
+
type: "assistant",
|
|
249
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
250
|
+
message: { role: "assistant", content: blocks },
|
|
251
|
+
} satisfies AssistantEntry);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (recType === "tool.execution_start") {
|
|
256
|
+
const callId = data.toolCallId as string | undefined;
|
|
257
|
+
const name = (data.toolName as string) ?? "tool";
|
|
258
|
+
const args = (data.arguments as Record<string, unknown>) ?? {};
|
|
259
|
+
const id = callId ?? `${date.getTime()}-${name}`;
|
|
260
|
+
const toolUse: ToolUseBlock = {
|
|
261
|
+
type: "tool_use",
|
|
262
|
+
id,
|
|
263
|
+
name,
|
|
264
|
+
input: args,
|
|
265
|
+
};
|
|
266
|
+
const entry: AssistantEntry = {
|
|
267
|
+
type: "assistant",
|
|
268
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
269
|
+
message: { role: "assistant", content: [toolUse] },
|
|
270
|
+
};
|
|
271
|
+
entries.push(entry);
|
|
272
|
+
if (callId) {
|
|
273
|
+
toolUseById.set(callId, toolUse);
|
|
274
|
+
toolUseStartMs.set(callId, date.getTime());
|
|
275
|
+
}
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (recType === "tool.execution_complete") {
|
|
280
|
+
const callId = data.toolCallId as string | undefined;
|
|
281
|
+
const block = callId ? toolUseById.get(callId) : undefined;
|
|
282
|
+
if (block) {
|
|
283
|
+
const startMs = toolUseStartMs.get(callId!) ?? date.getTime();
|
|
284
|
+
const result = (data.result as CopilotToolResult | undefined) ?? {};
|
|
285
|
+
const telemetry = (data.toolTelemetry as CopilotToolTelemetry | undefined) ?? {};
|
|
286
|
+
const reportedMs =
|
|
287
|
+
telemetry.metrics?.commandTimeMs ?? telemetry.metrics?.durationMs ?? null;
|
|
288
|
+
const durationMs =
|
|
289
|
+
typeof reportedMs === "number" && reportedMs >= 0
|
|
290
|
+
? reportedMs
|
|
291
|
+
: Math.max(0, date.getTime() - startMs);
|
|
292
|
+
const content = result.detailedContent ?? result.content ?? "";
|
|
293
|
+
block.result = {
|
|
294
|
+
timestamp,
|
|
295
|
+
timestampFormatted: formatTimestamp(date),
|
|
296
|
+
content: typeof content === "string" ? content : JSON.stringify(content),
|
|
297
|
+
durationMs,
|
|
298
|
+
durationFormatted: formatDuration(durationMs),
|
|
299
|
+
};
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
// Orphan tool result — preserve as system.
|
|
303
|
+
entries.push({
|
|
304
|
+
type: "system",
|
|
305
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
306
|
+
raw: rawCopy,
|
|
307
|
+
} satisfies GenericEntry);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// assistant.turn_start, assistant.turn_end, session.model_change,
|
|
312
|
+
// session.shutdown, and any unrecognized type — preserve raw so nothing is
|
|
313
|
+
// silently dropped, but keep them out of the structured user/assistant
|
|
314
|
+
// timeline (they're scaffolding events).
|
|
315
|
+
entries.push({
|
|
316
|
+
type: "system",
|
|
317
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
318
|
+
raw: rawCopy,
|
|
319
|
+
} satisfies GenericEntry);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (entries.length > 500) await new Promise<void>((r) => setImmediate(r));
|
|
323
|
+
entries.sort((a, b) => a.timestampMs - b.timestampMs);
|
|
324
|
+
|
|
325
|
+
return { entries, rawLines, cwd };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Public loader ──
|
|
329
|
+
|
|
330
|
+
export interface CopilotSessionLogData {
|
|
331
|
+
entries: LogEntry[];
|
|
332
|
+
rawLines: Record<string, unknown>[];
|
|
333
|
+
cwd?: string;
|
|
334
|
+
filePath: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function getCopilotSessionLog(sessionId: string): Promise<CopilotSessionLogData | null> {
|
|
338
|
+
const filePath = findCopilotTranscript(sessionId);
|
|
339
|
+
if (!filePath) return null;
|
|
340
|
+
// findCopilotTranscript only proves existence at lookup time; the file can be
|
|
341
|
+
// removed/rotated before this readFile lands. Preserve the nullable contract
|
|
342
|
+
// instead of throwing into the session page.
|
|
343
|
+
let fileContent: string;
|
|
344
|
+
try {
|
|
345
|
+
fileContent = await readFile(filePath, "utf-8");
|
|
346
|
+
} catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const { entries, rawLines, cwd } = await parseCopilotLog(fileContent, "session");
|
|
350
|
+
// Fall back to workspace.yaml if events.jsonl didn't expose a session.start.
|
|
351
|
+
const resolvedCwd = cwd ?? readCopilotWorkspaceCwd(sessionId);
|
|
352
|
+
return { entries, rawLines, cwd: resolvedCwd, filePath };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export const getCachedCopilotSessionLog = runtimeCache(
|
|
356
|
+
(sessionId: string) => getCopilotSessionLog(sessionId),
|
|
357
|
+
60,
|
|
358
|
+
{ maxSize: 50 },
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// ── Test helpers ──
|
|
362
|
+
|
|
363
|
+
/** For tests: read raw stat of the events.jsonl path, returning null on miss. */
|
|
364
|
+
export function _statTranscript(sessionId: string): { mtimeMs: number } | null {
|
|
365
|
+
const path = findCopilotTranscript(sessionId);
|
|
366
|
+
if (!path) return null;
|
|
367
|
+
try {
|
|
368
|
+
const s = statSync(path);
|
|
369
|
+
return { mtimeMs: s.mtimeMs };
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** For tests: list session IDs found in session-state/. */
|
|
376
|
+
export function _listSessionIds(): string[] {
|
|
377
|
+
try {
|
|
378
|
+
return readdirSync(getCopilotSessionStateRoot(), { withFileTypes: true })
|
|
379
|
+
.filter((e) => e.isDirectory())
|
|
380
|
+
.map((e) => e.name);
|
|
381
|
+
} catch {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Surface a sync read variant used by lower-level code paths. */
|
|
387
|
+
export function readCopilotTranscriptSync(sessionId: string): string | null {
|
|
388
|
+
const path = findCopilotTranscript(sessionId);
|
|
389
|
+
if (!path) return null;
|
|
390
|
+
try {
|
|
391
|
+
return readFileSync(path, "utf-8");
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|