failproofai 0.0.9-beta.1 → 0.0.9
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 +6 -6
- 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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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 +2 -2
- 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 +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -2
- 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 +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +2 -2
- 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/build-manifest.json +3 -3
- 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/build-manifest.json +3 -3
- 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 +4 -3
- 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/build-manifest.json +3 -3
- 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]__0g72weg._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.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]__01743wx._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0.f_cyx._.js → [root-of-the-server]__01g_w_e._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0dub28-._.js → [root-of-the-server]__0_b7pgn._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +18 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +3 -0
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_utils_ts_068jk73._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0zaq1hm._.js → node_modules_0ttbz1~._.js} +2 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- 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/{0_c_yox08g_44.js → 01q52wg_amm60.js} +2 -2
- package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +1 -0
- package/.next/standalone/.next/static/chunks/{0bghqwo4iloy0.js → 0756i.7omnnl6.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0p5zh2diw90a1.js → 095l4hc7-h.~~.js} +1 -1
- package/.next/standalone/.next/static/chunks/09ose_165ra4d.js +1 -0
- package/.next/standalone/.next/static/chunks/0n-_j_6fo6jex.js +6 -0
- package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +1 -0
- package/.next/standalone/.next/static/chunks/{0ufq8smh~i7wc.js → 0pr7k36o_.du1.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0z-jh701rc~j8.js → 0t~iusm_fxoao.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0jryicwtm9z2g.js → 0u-ys71jc4y68.js} +3 -3
- package/.next/standalone/.next/static/chunks/0zig0fh30t6ou.js +1 -0
- package/.next/standalone/.next/static/chunks/{0w1f.k~gi-y6..js → 11kt_9zaooda3.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0kzk5-mh1_x53.js → 12simlrcfk3g2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-0s36is87fc9r2.js → turbopack-0o7k.hakttp4k.js} +1 -1
- package/.next/standalone/app/components/cli-badge.tsx +24 -0
- package/.next/standalone/app/components/project-list.tsx +13 -7
- package/.next/standalone/app/components/sessions-list.tsx +4 -2
- package/.next/standalone/app/policies/hooks-client.tsx +66 -10
- package/.next/standalone/app/project/[name]/page.tsx +49 -22
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +51 -19
- package/.next/standalone/components/reach-developers.tsx +6 -1
- package/.next/standalone/lib/codex-projects.ts +250 -0
- package/.next/standalone/lib/codex-sessions.ts +414 -0
- package/.next/standalone/lib/format-date.ts +21 -0
- package/.next/standalone/lib/log-entries.ts +3 -3
- package/.next/standalone/lib/paths.ts +13 -0
- package/.next/standalone/lib/projects.ts +57 -3
- package/.next/standalone/lib/utils.ts +6 -22
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +1 -0
- package/dist/cli.mjs +1042 -122
- package/lib/codex-projects.ts +250 -0
- package/lib/codex-sessions.ts +414 -0
- package/lib/format-date.ts +21 -0
- package/lib/log-entries.ts +3 -3
- package/lib/paths.ts +13 -0
- package/lib/projects.ts +57 -3
- package/lib/utils.ts +6 -22
- package/package.json +1 -1
- package/scripts/launch.ts +2 -1
- package/src/hooks/builtin-policies.ts +7 -1
- package/src/hooks/hook-activity-store.ts +3 -0
- package/src/hooks/manager.ts +1 -1
- package/src/hooks/resolve-permission-mode.ts +6 -91
- package/.next/standalone/.next/server/chunks/[externals]__080wern._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03rd.z8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e74wa-._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vu.o-3._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +0 -17
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0zqcovi._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__105.l_7._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0uy6m~m._.js +0 -3
- package/.next/standalone/.next/static/chunks/00b5h4r1el.6f.js +0 -1
- package/.next/standalone/.next/static/chunks/0fw2h.g66c0h3.js +0 -1
- package/.next/standalone/.next/static/chunks/0gu87mlr5ssnt.js +0 -6
- package/.next/standalone/.next/static/chunks/0igf3xbisp1lx.js +0 -1
- package/.next/standalone/.next/static/chunks/0vwqucikost_q.js +0 -1
- /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → A_Ax17P33facL0OmIwFXj}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → A_Ax17P33facL0OmIwFXj}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{CiVeb_yiVt-O2JYrzGzB7 → A_Ax17P33facL0OmIwFXj}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex (OpenAI) session transcript discovery + JSONL parser.
|
|
3
|
+
*
|
|
4
|
+
* Codex stores transcripts at:
|
|
5
|
+
* ~/.codex/sessions/<YYYY>/<MM>/<DD>/<file containing sessionId>.jsonl
|
|
6
|
+
*
|
|
7
|
+
* The schema is uniform `{ timestamp, type, payload }` records:
|
|
8
|
+
* - type: session_meta — metadata (cwd, model, base instructions)
|
|
9
|
+
* - type: turn_context — per-turn config (approval_policy, sandbox)
|
|
10
|
+
* - type: response_item — message / function_call / function_call_output
|
|
11
|
+
* - type: event_msg — task_started, user_message, agent_message,
|
|
12
|
+
* exec_command_begin/end, token_count, …
|
|
13
|
+
*
|
|
14
|
+
* `parseCodexLog` maps these into the same `LogEntry` shapes the Claude
|
|
15
|
+
* parser produces (`lib/log-entries.ts`) so the existing log viewer renders
|
|
16
|
+
* Codex sessions without any UI-side branching.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync, readdirSync, existsSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { runtimeCache } from "./runtime-cache";
|
|
23
|
+
import {
|
|
24
|
+
baseEntry,
|
|
25
|
+
formatTimestamp,
|
|
26
|
+
type LogEntry,
|
|
27
|
+
type UserEntry,
|
|
28
|
+
type AssistantEntry,
|
|
29
|
+
type GenericEntry,
|
|
30
|
+
type QueueOperationEntry,
|
|
31
|
+
type ContentBlock,
|
|
32
|
+
type ToolUseBlock,
|
|
33
|
+
type LogSource,
|
|
34
|
+
} from "./log-entries";
|
|
35
|
+
import { formatDuration } from "./format-duration";
|
|
36
|
+
|
|
37
|
+
// ── Transcript discovery ──
|
|
38
|
+
|
|
39
|
+
const CACHE_PATH = join(homedir(), ".failproofai", "cache", "codex-session-paths.json");
|
|
40
|
+
|
|
41
|
+
function readCache(): Record<string, string> {
|
|
42
|
+
try {
|
|
43
|
+
if (!existsSync(CACHE_PATH)) return {};
|
|
44
|
+
return JSON.parse(readFileSync(CACHE_PATH, "utf-8")) as Record<string, string>;
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeCacheEntry(sessionId: string, path: string): void {
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(dirname(CACHE_PATH), { recursive: true });
|
|
53
|
+
const cache = readCache();
|
|
54
|
+
cache[sessionId] = path;
|
|
55
|
+
writeFileSync(CACHE_PATH, JSON.stringify(cache), "utf-8");
|
|
56
|
+
} catch {
|
|
57
|
+
// Cache is best-effort
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function dirSearch(dir: string, sessionId: string): string | null {
|
|
62
|
+
try {
|
|
63
|
+
for (const f of readdirSync(dir, { withFileTypes: true })) {
|
|
64
|
+
if (f.isFile() && f.name.includes(sessionId) && f.name.endsWith(".jsonl")) {
|
|
65
|
+
return join(dir, f.name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// dir doesn't exist or unreadable
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Locate a Codex transcript by sessionId. Tries the cache, then today/
|
|
76
|
+
* yesterday's date directories, then a full tree scan as fallback.
|
|
77
|
+
* Synchronous so the hook hot path can call it without awaits.
|
|
78
|
+
*/
|
|
79
|
+
export function findCodexTranscript(sessionId: string): string | null {
|
|
80
|
+
const cache = readCache();
|
|
81
|
+
const cached = cache[sessionId];
|
|
82
|
+
if (cached && existsSync(cached)) return cached;
|
|
83
|
+
|
|
84
|
+
const root = join(homedir(), ".codex", "sessions");
|
|
85
|
+
|
|
86
|
+
const today = new Date();
|
|
87
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
88
|
+
const datedDirs = [today, yesterday].map((d) => {
|
|
89
|
+
const y = String(d.getUTCFullYear());
|
|
90
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
91
|
+
const day = String(d.getUTCDate()).padStart(2, "0");
|
|
92
|
+
return join(root, y, m, day);
|
|
93
|
+
});
|
|
94
|
+
for (const dir of datedDirs) {
|
|
95
|
+
const hit = dirSearch(dir, sessionId);
|
|
96
|
+
if (hit) {
|
|
97
|
+
writeCacheEntry(sessionId, hit);
|
|
98
|
+
return hit;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
for (const y of readdirSync(root, { withFileTypes: true })) {
|
|
104
|
+
if (!y.isDirectory()) continue;
|
|
105
|
+
for (const m of readdirSync(join(root, y.name), { withFileTypes: true })) {
|
|
106
|
+
if (!m.isDirectory()) continue;
|
|
107
|
+
for (const d of readdirSync(join(root, y.name, m.name), { withFileTypes: true })) {
|
|
108
|
+
if (!d.isDirectory()) continue;
|
|
109
|
+
const hit = dirSearch(join(root, y.name, m.name, d.name), sessionId);
|
|
110
|
+
if (hit) {
|
|
111
|
+
writeCacheEntry(sessionId, hit);
|
|
112
|
+
return hit;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Session may not have flushed yet, or the path doesn't exist
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Parser ──
|
|
124
|
+
|
|
125
|
+
interface CodexRecord {
|
|
126
|
+
timestamp?: string;
|
|
127
|
+
type?: string;
|
|
128
|
+
payload?: Record<string, unknown>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface CodexContentBlock {
|
|
132
|
+
type?: string;
|
|
133
|
+
text?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface CodexParseResult {
|
|
137
|
+
entries: LogEntry[];
|
|
138
|
+
rawLines: Record<string, unknown>[];
|
|
139
|
+
/** Working directory pulled from the first session_meta record, when present. */
|
|
140
|
+
cwd?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function safeJsonParse(s: string | undefined): Record<string, unknown> {
|
|
144
|
+
if (!s) return {};
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(s) as Record<string, unknown>;
|
|
147
|
+
} catch {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function joinTexts(blocks: CodexContentBlock[] | undefined, wantedType: "input_text" | "output_text"): string {
|
|
153
|
+
if (!Array.isArray(blocks)) return "";
|
|
154
|
+
return blocks
|
|
155
|
+
.filter((b) => b?.type === wantedType && typeof b.text === "string")
|
|
156
|
+
.map((b) => b.text as string)
|
|
157
|
+
.join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Parse a Codex JSONL transcript into `LogEntry[]` plus the raw lines.
|
|
162
|
+
* Yields to the event loop every 200 lines so big transcripts don't block.
|
|
163
|
+
*/
|
|
164
|
+
export async function parseCodexLog(
|
|
165
|
+
fileContent: string,
|
|
166
|
+
source: LogSource = "session",
|
|
167
|
+
): Promise<CodexParseResult> {
|
|
168
|
+
const lines = fileContent.split("\n").filter((line) => line.trim() !== "");
|
|
169
|
+
|
|
170
|
+
const entries: LogEntry[] = [];
|
|
171
|
+
const rawLines: Record<string, unknown>[] = [];
|
|
172
|
+
// call_id → tool_use block, so we can attach exec_command_end results back to the originating call.
|
|
173
|
+
const toolUseById = new Map<string, ToolUseBlock>();
|
|
174
|
+
// call_id → tool_use entry timestamp, used to compute durationMs from end records that lack a duration.
|
|
175
|
+
const toolUseStartMs = new Map<string, number>();
|
|
176
|
+
let cwd: string | undefined;
|
|
177
|
+
let seenTaskStart = false;
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < lines.length; i++) {
|
|
180
|
+
if (i > 0 && i % 200 === 0) await new Promise<void>((r) => setImmediate(r));
|
|
181
|
+
|
|
182
|
+
const line = lines[i];
|
|
183
|
+
let raw: CodexRecord;
|
|
184
|
+
try {
|
|
185
|
+
raw = JSON.parse(line) as CodexRecord;
|
|
186
|
+
} catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rawCopy = { ...(raw as Record<string, unknown>), _source: source };
|
|
191
|
+
rawLines.push(rawCopy);
|
|
192
|
+
|
|
193
|
+
const timestamp = raw.timestamp;
|
|
194
|
+
if (!timestamp) continue;
|
|
195
|
+
const date = new Date(timestamp);
|
|
196
|
+
if (Number.isNaN(date.getTime())) continue;
|
|
197
|
+
|
|
198
|
+
const recType = raw.type;
|
|
199
|
+
const payload = raw.payload ?? {};
|
|
200
|
+
|
|
201
|
+
if (recType === "session_meta") {
|
|
202
|
+
const c = payload.cwd;
|
|
203
|
+
if (typeof c === "string" && !cwd) cwd = c;
|
|
204
|
+
entries.push({
|
|
205
|
+
type: "system",
|
|
206
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
207
|
+
raw: rawCopy,
|
|
208
|
+
} satisfies GenericEntry);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (recType === "response_item") {
|
|
213
|
+
const subType = payload.type as string | undefined;
|
|
214
|
+
|
|
215
|
+
if (subType === "message") {
|
|
216
|
+
const role = payload.role as string | undefined;
|
|
217
|
+
const content = payload.content as CodexContentBlock[] | undefined;
|
|
218
|
+
|
|
219
|
+
if (role === "user" || role === "developer") {
|
|
220
|
+
const text = joinTexts(content, "input_text");
|
|
221
|
+
if (!text) continue;
|
|
222
|
+
const message = role === "developer" ? `[developer] ${text}` : text;
|
|
223
|
+
entries.push({
|
|
224
|
+
type: "user",
|
|
225
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
226
|
+
message: { role: "user", content: message },
|
|
227
|
+
} satisfies UserEntry);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (role === "assistant") {
|
|
232
|
+
const text = joinTexts(content, "output_text");
|
|
233
|
+
if (!text) continue;
|
|
234
|
+
const blocks: ContentBlock[] = [{ type: "text", text }];
|
|
235
|
+
entries.push({
|
|
236
|
+
type: "assistant",
|
|
237
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
238
|
+
message: { role: "assistant", content: blocks },
|
|
239
|
+
} satisfies AssistantEntry);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Unknown role — preserve as system so nothing is lost.
|
|
244
|
+
entries.push({
|
|
245
|
+
type: "system",
|
|
246
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
247
|
+
raw: rawCopy,
|
|
248
|
+
} satisfies GenericEntry);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (subType === "function_call") {
|
|
253
|
+
const callId = payload.call_id as string | undefined;
|
|
254
|
+
const name = (payload.name as string | undefined) ?? "function_call";
|
|
255
|
+
const input = safeJsonParse(payload.arguments as string | undefined);
|
|
256
|
+
const toolUse: ToolUseBlock = {
|
|
257
|
+
type: "tool_use",
|
|
258
|
+
id: callId ?? `${timestamp}-${name}`,
|
|
259
|
+
name,
|
|
260
|
+
input,
|
|
261
|
+
};
|
|
262
|
+
const entry: AssistantEntry = {
|
|
263
|
+
type: "assistant",
|
|
264
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
265
|
+
message: { role: "assistant", content: [toolUse] },
|
|
266
|
+
};
|
|
267
|
+
entries.push(entry);
|
|
268
|
+
if (callId) {
|
|
269
|
+
toolUseById.set(callId, toolUse);
|
|
270
|
+
toolUseStartMs.set(callId, date.getTime());
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (subType === "function_call_output") {
|
|
276
|
+
const callId = payload.call_id as string | undefined;
|
|
277
|
+
const block = callId ? toolUseById.get(callId) : undefined;
|
|
278
|
+
if (block) {
|
|
279
|
+
const startMs = toolUseStartMs.get(callId!) ?? date.getTime();
|
|
280
|
+
const duration = Math.max(0, date.getTime() - startMs);
|
|
281
|
+
block.result = {
|
|
282
|
+
timestamp,
|
|
283
|
+
timestampFormatted: formatTimestamp(date),
|
|
284
|
+
content: typeof payload.output === "string" ? (payload.output as string) : JSON.stringify(payload.output),
|
|
285
|
+
durationMs: duration,
|
|
286
|
+
durationFormatted: formatDuration(duration),
|
|
287
|
+
};
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// Orphan output — preserve as system.
|
|
291
|
+
entries.push({
|
|
292
|
+
type: "system",
|
|
293
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
294
|
+
raw: rawCopy,
|
|
295
|
+
} satisfies GenericEntry);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Unknown response_item subtype — preserve raw.
|
|
300
|
+
entries.push({
|
|
301
|
+
type: "system",
|
|
302
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
303
|
+
raw: rawCopy,
|
|
304
|
+
} satisfies GenericEntry);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (recType === "event_msg") {
|
|
309
|
+
const subType = payload.type as string | undefined;
|
|
310
|
+
|
|
311
|
+
if (subType === "task_started") {
|
|
312
|
+
const label: QueueOperationEntry["label"] = seenTaskStart ? "Session Resumed" : "Session Started";
|
|
313
|
+
seenTaskStart = true;
|
|
314
|
+
entries.push({
|
|
315
|
+
type: "queue-operation",
|
|
316
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
317
|
+
label,
|
|
318
|
+
} satisfies QueueOperationEntry);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (subType === "exec_command_end") {
|
|
323
|
+
const callId = payload.call_id as string | undefined;
|
|
324
|
+
const block = callId ? toolUseById.get(callId) : undefined;
|
|
325
|
+
if (block) {
|
|
326
|
+
const duration = payload.duration as { secs?: number; nanos?: number } | undefined;
|
|
327
|
+
const durationMs = duration
|
|
328
|
+
? (duration.secs ?? 0) * 1000 + Math.round((duration.nanos ?? 0) / 1e6)
|
|
329
|
+
: Math.max(0, date.getTime() - (toolUseStartMs.get(callId!) ?? date.getTime()));
|
|
330
|
+
const aggregated = payload.aggregated_output;
|
|
331
|
+
block.result = {
|
|
332
|
+
timestamp,
|
|
333
|
+
timestampFormatted: formatTimestamp(date),
|
|
334
|
+
content: typeof aggregated === "string" ? aggregated : JSON.stringify(aggregated),
|
|
335
|
+
durationMs,
|
|
336
|
+
durationFormatted: formatDuration(durationMs),
|
|
337
|
+
};
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// Orphan exec end — preserve as system.
|
|
341
|
+
entries.push({
|
|
342
|
+
type: "system",
|
|
343
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
344
|
+
raw: rawCopy,
|
|
345
|
+
} satisfies GenericEntry);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (subType === "user_message" || subType === "agent_message") {
|
|
350
|
+
// Already rendered via the corresponding response_item; skip to avoid duplicates.
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Other event_msg subtypes (token_count, exec_command_begin, etc.) — preserve raw.
|
|
355
|
+
entries.push({
|
|
356
|
+
type: "system",
|
|
357
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
358
|
+
raw: rawCopy,
|
|
359
|
+
} satisfies GenericEntry);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// turn_context and any unrecognized type — preserve raw so nothing is silently dropped.
|
|
364
|
+
entries.push({
|
|
365
|
+
type: "system",
|
|
366
|
+
...baseEntry(rawCopy, timestamp, date, source),
|
|
367
|
+
raw: rawCopy,
|
|
368
|
+
} satisfies GenericEntry);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (entries.length > 500) await new Promise<void>((r) => setImmediate(r));
|
|
372
|
+
entries.sort((a, b) => a.timestampMs - b.timestampMs);
|
|
373
|
+
|
|
374
|
+
return { entries, rawLines, cwd };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Public loader ──
|
|
378
|
+
|
|
379
|
+
export interface CodexSessionLogData {
|
|
380
|
+
entries: LogEntry[];
|
|
381
|
+
rawLines: Record<string, unknown>[];
|
|
382
|
+
cwd?: string;
|
|
383
|
+
filePath: string;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function getCodexSessionLog(sessionId: string): Promise<CodexSessionLogData | null> {
|
|
387
|
+
const filePath = findCodexTranscript(sessionId);
|
|
388
|
+
if (!filePath) return null;
|
|
389
|
+
const fileContent = await readFile(filePath, "utf-8");
|
|
390
|
+
const { entries, rawLines, cwd } = await parseCodexLog(fileContent, "session");
|
|
391
|
+
return { entries, rawLines, cwd, filePath };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export const getCachedCodexSessionLog = runtimeCache(
|
|
395
|
+
(sessionId: string) => getCodexSessionLog(sessionId),
|
|
396
|
+
60,
|
|
397
|
+
{ maxSize: 50 },
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// ── Test helpers ──
|
|
401
|
+
|
|
402
|
+
/** For tests: inspect cache file path. */
|
|
403
|
+
export function _getCacheFilePath(): string {
|
|
404
|
+
return CACHE_PATH;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** For tests: confirm the file exists at a path. Wraps fs to keep tests minimal. */
|
|
408
|
+
export function _statFile(path: string): { isFile: boolean } | null {
|
|
409
|
+
try {
|
|
410
|
+
return { isFile: statSync(path).isFile() };
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a date to a readable string format (e.g., "Jan 15, 2024, 3:45 PM").
|
|
3
|
+
*
|
|
4
|
+
* Creates a new Intl.DateTimeFormat on each call intentionally — this runs
|
|
5
|
+
* server-side where there's no shared state concern. The client-side hot-path
|
|
6
|
+
* formatter in lib/log-format.ts caches its instance at module scope instead.
|
|
7
|
+
*
|
|
8
|
+
* Lives in its own module (rather than lib/utils.ts) so server-side callers
|
|
9
|
+
* — including the hook handler's transitive imports — don't need to pull in
|
|
10
|
+
* clsx/tailwind-merge just to format a date.
|
|
11
|
+
*/
|
|
12
|
+
export function formatDate(date: Date): string {
|
|
13
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
14
|
+
month: "short",
|
|
15
|
+
day: "numeric",
|
|
16
|
+
year: "numeric",
|
|
17
|
+
hour: "numeric",
|
|
18
|
+
minute: "2-digit",
|
|
19
|
+
hour12: true,
|
|
20
|
+
}).format(date);
|
|
21
|
+
}
|
|
@@ -5,7 +5,7 @@ import { resolveProjectPath } from "./projects";
|
|
|
5
5
|
import { resolveSubagentPath } from "./resolve-subagent-path";
|
|
6
6
|
import { runtimeCache } from "./runtime-cache";
|
|
7
7
|
import { batchAll } from "./concurrency";
|
|
8
|
-
import { formatDate } from "./
|
|
8
|
+
import { formatDate } from "./format-date";
|
|
9
9
|
import { formatDuration } from "./format-duration";
|
|
10
10
|
|
|
11
11
|
// ── Source Tagging ──
|
|
@@ -115,14 +115,14 @@ export type LogEntryType = LogEntry["type"];
|
|
|
115
115
|
|
|
116
116
|
// ── Helpers ──
|
|
117
117
|
|
|
118
|
-
function formatTimestamp(date: Date): string {
|
|
118
|
+
export function formatTimestamp(date: Date): string {
|
|
119
119
|
const base = formatDate(date);
|
|
120
120
|
const ms = date.getMilliseconds().toString().padStart(3, "0");
|
|
121
121
|
return `${base}.${ms}`;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** Shared base fields present on every log entry. */
|
|
125
|
-
function baseEntry(raw: Record<string, unknown>, timestamp: string, date: Date, source: LogSource) {
|
|
125
|
+
export function baseEntry(raw: Record<string, unknown>, timestamp: string, date: Date, source: LogSource) {
|
|
126
126
|
return {
|
|
127
127
|
_source: source,
|
|
128
128
|
uuid: (raw.uuid as string) || "",
|
|
@@ -29,6 +29,19 @@ export function decodeFolderName(name: string): string {
|
|
|
29
29
|
return name.replace(/-/g, "/");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Encodes a filesystem path into a Claude-compatible project folder name.
|
|
34
|
+
* Inverse of `decodeFolderName`.
|
|
35
|
+
*/
|
|
36
|
+
export function encodeFolderName(path: string): string {
|
|
37
|
+
// Windows drive-letter pattern: "C:/code/project" → "C--code-project"
|
|
38
|
+
const driveMatch = /^([A-Za-z]):[\\/](.*)$/.exec(path);
|
|
39
|
+
if (driveMatch) {
|
|
40
|
+
return driveMatch[1] + "--" + driveMatch[2].replace(/[\\/]/g, "-");
|
|
41
|
+
}
|
|
42
|
+
return path.replace(/[\\/]/g, "-");
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export function getClaudeProjectsPath(): string {
|
|
33
46
|
// Check if path is provided via environment variable
|
|
34
47
|
const envPath = process.env.CLAUDE_PROJECTS_PATH;
|
|
@@ -11,17 +11,24 @@ import { getClaudeProjectsPath } from "./paths";
|
|
|
11
11
|
import { runtimeCache } from "./runtime-cache";
|
|
12
12
|
import { batchAll } from "./concurrency";
|
|
13
13
|
import { logWarn, logError } from "./logger";
|
|
14
|
-
import { formatDate } from "./
|
|
14
|
+
import { formatDate } from "./format-date";
|
|
15
15
|
|
|
16
16
|
export const UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
17
17
|
export const PATH_TRAVERSAL_RE = /(^|[\\/])\.\.($|[\\/])/;
|
|
18
18
|
|
|
19
|
+
export type ProjectCli = "claude" | "codex";
|
|
20
|
+
|
|
19
21
|
export interface ProjectFolder {
|
|
20
22
|
name: string;
|
|
21
23
|
path: string;
|
|
22
24
|
isDirectory: boolean;
|
|
23
25
|
lastModified: Date;
|
|
24
26
|
lastModifiedFormatted?: string; // Pre-formatted date string to avoid hydration issues
|
|
27
|
+
/**
|
|
28
|
+
* Which agent CLIs this project's data was found in. Multiple entries when
|
|
29
|
+
* the same cwd has both Claude and Codex transcripts; rendered as badges.
|
|
30
|
+
*/
|
|
31
|
+
cli: ProjectCli[];
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export interface SessionFile {
|
|
@@ -30,6 +37,9 @@ export interface SessionFile {
|
|
|
30
37
|
lastModified: Date;
|
|
31
38
|
lastModifiedFormatted?: string;
|
|
32
39
|
sessionId?: string;
|
|
40
|
+
/** Originating agent CLI. Set when the session list mixes Claude + Codex sources
|
|
41
|
+
* so the table can render a per-row CLI badge. */
|
|
42
|
+
cli?: ProjectCli;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
/** Stats a path and returns mtime. Falls back to epoch (1970-01-01) on error
|
|
@@ -54,7 +64,7 @@ async function safeReaddir(dirPath: string) {
|
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
async function getClaudeProjectFolders(): Promise<ProjectFolder[]> {
|
|
58
68
|
try {
|
|
59
69
|
const projectsPath = getClaudeProjectsPath();
|
|
60
70
|
const entries = await safeReaddir(projectsPath);
|
|
@@ -72,6 +82,7 @@ export async function getProjectFolders(): Promise<ProjectFolder[]> {
|
|
|
72
82
|
isDirectory: true,
|
|
73
83
|
lastModified: mtime,
|
|
74
84
|
lastModifiedFormatted: formatDate(mtime),
|
|
85
|
+
cli: ["claude"],
|
|
75
86
|
} as ProjectFolder;
|
|
76
87
|
}),
|
|
77
88
|
16,
|
|
@@ -83,11 +94,53 @@ export async function getProjectFolders(): Promise<ProjectFolder[]> {
|
|
|
83
94
|
folders.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
84
95
|
return folders;
|
|
85
96
|
} catch (error) {
|
|
86
|
-
logError("Error reading project folders:", error);
|
|
97
|
+
logError("Error reading Claude project folders:", error);
|
|
87
98
|
return [];
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
/** Merges Claude + Codex project lists by encoded folder name. When both sources have
|
|
103
|
+
* the same name, keeps the Claude entry's `path` (so the Path column still points at
|
|
104
|
+
* `~/.claude/projects/<encoded>`), unions the `cli` arrays in [claude, codex] order,
|
|
105
|
+
* and takes the newer `lastModified`. */
|
|
106
|
+
function mergeProjectFolders(claude: ProjectFolder[], codex: ProjectFolder[]): ProjectFolder[] {
|
|
107
|
+
const byName = new Map<string, ProjectFolder>();
|
|
108
|
+
for (const f of claude) byName.set(f.name, { ...f, cli: [...f.cli] });
|
|
109
|
+
for (const f of codex) {
|
|
110
|
+
const existing = byName.get(f.name);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
byName.set(f.name, { ...f, cli: [...f.cli] });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const mergedCli: ProjectCli[] = [...existing.cli];
|
|
116
|
+
for (const c of f.cli) if (!mergedCli.includes(c)) mergedCli.push(c);
|
|
117
|
+
const newer = f.lastModified.getTime() > existing.lastModified.getTime() ? f : existing;
|
|
118
|
+
byName.set(f.name, {
|
|
119
|
+
...existing,
|
|
120
|
+
cli: mergedCli,
|
|
121
|
+
lastModified: newer.lastModified,
|
|
122
|
+
lastModifiedFormatted: newer.lastModifiedFormatted,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const merged = Array.from(byName.values());
|
|
126
|
+
merged.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
127
|
+
return merged;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function getProjectFolders(): Promise<ProjectFolder[]> {
|
|
131
|
+
// Lazy import to keep `lib/codex-projects.ts` out of the dep graph for callers that
|
|
132
|
+
// only need Claude helpers (e.g. CLI codepaths).
|
|
133
|
+
const { getCodexProjects } = await import("./codex-projects");
|
|
134
|
+
const [claude, codex] = await Promise.all([
|
|
135
|
+
getClaudeProjectFolders(),
|
|
136
|
+
getCodexProjects().catch((error) => {
|
|
137
|
+
logError("Error reading Codex projects:", error);
|
|
138
|
+
return [] as ProjectFolder[];
|
|
139
|
+
}),
|
|
140
|
+
]);
|
|
141
|
+
return mergeProjectFolders(claude, codex);
|
|
142
|
+
}
|
|
143
|
+
|
|
91
144
|
/**
|
|
92
145
|
* Gets the full path to a specific project folder
|
|
93
146
|
* @param projectName - Name of the project folder
|
|
@@ -157,6 +210,7 @@ export async function getSessionFiles(projectPath: string): Promise<SessionFile[
|
|
|
157
210
|
lastModified: mtime,
|
|
158
211
|
lastModifiedFormatted: formatDate(mtime),
|
|
159
212
|
sessionId: extractSessionId(entry.name),
|
|
213
|
+
cli: "claude",
|
|
160
214
|
} as SessionFile;
|
|
161
215
|
}),
|
|
162
216
|
16,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Tailwind class-name merger.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
4
|
+
* `cn()` combines `clsx` (conditional class composition) with `tailwind-merge`
|
|
5
|
+
* (last-wins conflict resolution for Tailwind utility classes).
|
|
6
|
+
*
|
|
7
|
+
* Date formatting lives in lib/format-date.ts — kept separate so server-side
|
|
8
|
+
* callers don't need to load clsx/tailwind-merge just to print a timestamp.
|
|
6
9
|
*/
|
|
7
10
|
import { clsx, type ClassValue } from "clsx";
|
|
8
11
|
import { twMerge } from "tailwind-merge";
|
|
@@ -10,22 +13,3 @@ import { twMerge } from "tailwind-merge";
|
|
|
10
13
|
export function cn(...inputs: ClassValue[]) {
|
|
11
14
|
return twMerge(clsx(inputs));
|
|
12
15
|
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Formats a date to a readable string format (e.g., "Jan 15, 2024, 3:45 PM").
|
|
16
|
-
*
|
|
17
|
-
* Creates a new Intl.DateTimeFormat on each call intentionally — this runs
|
|
18
|
-
* server-side where there's no shared state concern. The client-side hot-path
|
|
19
|
-
* formatter in lib/log-format.ts caches its instance at module scope instead.
|
|
20
|
-
*/
|
|
21
|
-
export function formatDate(date: Date): string {
|
|
22
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
23
|
-
month: "short",
|
|
24
|
-
day: "numeric",
|
|
25
|
-
year: "numeric",
|
|
26
|
-
hour: "numeric",
|
|
27
|
-
minute: "2-digit",
|
|
28
|
-
hour12: true,
|
|
29
|
-
}).format(date);
|
|
30
|
-
}
|
|
31
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "failproofai",
|
|
3
|
-
"version": "0.0.9
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"failproofai": "./dist/cli.mjs"
|