@scrylog/cli 0.1.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/LICENSE +21 -0
- package/README.md +836 -0
- package/package.json +75 -0
- package/src/api/docs/sessions.keys.ts +6 -0
- package/src/api/docs/sessions.ts +81 -0
- package/src/api/docs/system.keys.ts +5 -0
- package/src/api/docs/system.ts +66 -0
- package/src/api/docs/usage.keys.ts +3 -0
- package/src/api/docs/usage.ts +24 -0
- package/src/api/docs/webhooks.keys.ts +5 -0
- package/src/api/docs/webhooks.ts +78 -0
- package/src/api/index.ts +18 -0
- package/src/api/lib/auth.ts +43 -0
- package/src/api/lib/errors.ts +28 -0
- package/src/api/lib/route.ts +18 -0
- package/src/api/models/claude.ts +4 -0
- package/src/api/models/codex.ts +4 -0
- package/src/api/models/opencode.ts +11 -0
- package/src/api/routes/sessions.ts +47 -0
- package/src/api/routes/system.ts +103 -0
- package/src/api/routes/usage.ts +15 -0
- package/src/api/routes/webhooks/claude.ts +161 -0
- package/src/api/routes/webhooks/codex.ts +175 -0
- package/src/api/routes/webhooks/opencode.ts +107 -0
- package/src/api/routes.ts +93 -0
- package/src/api/schemas/common.ts +21 -0
- package/src/api/schemas/sessions.ts +72 -0
- package/src/api/schemas/usage.ts +50 -0
- package/src/api/schemas/webhooks/claude.ts +32 -0
- package/src/api/schemas/webhooks/codex.ts +34 -0
- package/src/api/schemas/webhooks/opencode.ts +29 -0
- package/src/api/services/claude-session.ts +9 -0
- package/src/api/services/codex-session.ts +8 -0
- package/src/api/services/opencode-session.ts +73 -0
- package/src/classifier.ts +206 -0
- package/src/cli/commands.ts +498 -0
- package/src/cli/prompt.ts +44 -0
- package/src/daemon/cli.ts +27 -0
- package/src/daemon/collectors/claude/README.md +108 -0
- package/src/daemon/collectors/claude/collector.ts +413 -0
- package/src/daemon/collectors/claude/discovery.ts +148 -0
- package/src/daemon/collectors/claude/hooks.ts +181 -0
- package/src/daemon/collectors/claude/index.ts +8 -0
- package/src/daemon/collectors/claude/jsonl.ts +85 -0
- package/src/daemon/collectors/claude/transcript.ts +246 -0
- package/src/daemon/collectors/codex/README.md +147 -0
- package/src/daemon/collectors/codex/collector.ts +661 -0
- package/src/daemon/collectors/codex/discovery.ts +104 -0
- package/src/daemon/collectors/codex/hooks.ts +179 -0
- package/src/daemon/collectors/codex/index.ts +7 -0
- package/src/daemon/collectors/codex/jsonl.ts +108 -0
- package/src/daemon/collectors/codex/transcript.ts +299 -0
- package/src/daemon/collectors/git.ts +295 -0
- package/src/daemon/collectors/opencode/README.md +106 -0
- package/src/daemon/collectors/opencode/collector.ts +543 -0
- package/src/daemon/collectors/opencode/discovery.ts +257 -0
- package/src/daemon/collectors/opencode/index.ts +5 -0
- package/src/daemon/collectors/opencode/session-builder.ts +59 -0
- package/src/daemon/collectors/opencode-db.ts +282 -0
- package/src/daemon/collectors/process.ts +201 -0
- package/src/daemon/collectors/tmux/README.md +128 -0
- package/src/daemon/collectors/tmux/collector.ts +472 -0
- package/src/daemon/collectors/tmux/enrichment.ts +241 -0
- package/src/daemon/collectors/tmux/index.ts +4 -0
- package/src/daemon/collectors/tmux/parse.ts +128 -0
- package/src/daemon/collectors/usage.ts +193 -0
- package/src/daemon/index.ts +207 -0
- package/src/daemon/scheduler.ts +86 -0
- package/src/daemon/store.ts +535 -0
- package/src/detect.ts +143 -0
- package/src/hooks/claude.ts +596 -0
- package/src/hooks/codex.ts +517 -0
- package/src/legacy/classifier.ts +215 -0
- package/src/legacy/detect.ts +143 -0
- package/src/mcp/client-server.ts +21 -0
- package/src/mcp/http-handler.ts +126 -0
- package/src/mcp/index.ts +61 -0
- package/src/mcp/resources/config-current.ts +19 -0
- package/src/mcp/resources/index.ts +11 -0
- package/src/mcp/resources/sessions-active.ts +19 -0
- package/src/mcp/resources/sessions-all.ts +19 -0
- package/src/mcp/schemas/sessions.ts +3 -0
- package/src/mcp/server.ts +17 -0
- package/src/mcp/services/api-adapter.ts +48 -0
- package/src/mcp/services/data-source.ts +22 -0
- package/src/mcp/services/store-adapter.ts +40 -0
- package/src/mcp/tools/get-active-sessions.ts +16 -0
- package/src/mcp/tools/get-session-children.ts +24 -0
- package/src/mcp/tools/get-session-messages.ts +24 -0
- package/src/mcp/tools/get-session.ts +23 -0
- package/src/mcp/tools/index.ts +15 -0
- package/src/mcp/tools/list-sessions.ts +16 -0
- package/src/neo-blessed.d.ts +4 -0
- package/src/oc/index.ts +302 -0
- package/src/service/manager.ts +242 -0
- package/src/shared/config.ts +478 -0
- package/src/shared/ipc.ts +80 -0
- package/src/shared/logger.ts +248 -0
- package/src/shared/paths.ts +73 -0
- package/src/shared/platform.ts +110 -0
- package/src/shared/session-view.ts +241 -0
- package/src/shared/types.ts +299 -0
- package/src/shared/utils.ts +339 -0
- package/src/tui/app.ts +920 -0
- package/src/tui/client.ts +149 -0
- package/src/tui/index.ts +181 -0
- package/src/types.ts +113 -0
- package/src/usage/README.md +323 -0
- package/src/usage/api.ts +99 -0
- package/src/usage/client.ts +3 -0
- package/src/usage/clients/anthropic-oauth-usage.ts +178 -0
- package/src/usage/clients/anthropic-web-usage.ts +245 -0
- package/src/usage/clients/claude-cli-probe.ts +187 -0
- package/src/usage/clients/codex-oauth-usage.ts +113 -0
- package/src/usage/clients/opencode-go-usage.ts +441 -0
- package/src/usage/index.ts +19 -0
- package/src/usage/mappers.ts +273 -0
- package/src/usage/planner.ts +54 -0
- package/src/usage/providers/claude.ts +113 -0
- package/src/usage/providers/codex.ts +34 -0
- package/src/usage/providers/opencode-go.ts +37 -0
- package/src/usage/resolver.ts +4 -0
- package/src/usage/resolvers/browser-cookies.ts +198 -0
- package/src/usage/resolvers/claude-auth.ts +99 -0
- package/src/usage/resolvers/claude-cli.ts +24 -0
- package/src/usage/resolvers/claude-web.ts +16 -0
- package/src/usage/resolvers/codex-auth.ts +117 -0
- package/src/usage/resolvers/opencode-go-web.ts +14 -0
- package/src/usage/types.ts +190 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, basename } from "node:path";
|
|
4
|
+
import type { ScrylogConfig } from "../../../shared/config";
|
|
5
|
+
import type { Logger } from "../../../shared/logger";
|
|
6
|
+
import { maybeRunCommand } from "../../../shared/utils";
|
|
7
|
+
|
|
8
|
+
export interface DiscoveredTranscript {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
path: string;
|
|
11
|
+
lastModified: number;
|
|
12
|
+
size: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getCodexDir(config: ScrylogConfig): string {
|
|
16
|
+
if (process.env.CODEX_HOME) return process.env.CODEX_HOME;
|
|
17
|
+
return config.codex.codexDir ?? join(homedir(), ".codex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function discoverTranscripts(codexDir: string, hookManagedIds?: ReadonlySet<string>): DiscoveredTranscript[] {
|
|
21
|
+
const results: DiscoveredTranscript[] = [];
|
|
22
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
// Date-partitioned sessions: sessions/YYYY/MM/DD/*.jsonl
|
|
25
|
+
const sessionsDir = join(codexDir, "sessions");
|
|
26
|
+
if (existsSync(sessionsDir)) {
|
|
27
|
+
try {
|
|
28
|
+
for (const year of readdirSync(sessionsDir)) {
|
|
29
|
+
const yearPath = join(sessionsDir, year);
|
|
30
|
+
if (!statSync(yearPath).isDirectory()) continue;
|
|
31
|
+
for (const month of readdirSync(yearPath)) {
|
|
32
|
+
const monthPath = join(yearPath, month);
|
|
33
|
+
if (!statSync(monthPath).isDirectory()) continue;
|
|
34
|
+
for (const day of readdirSync(monthPath)) {
|
|
35
|
+
const dayPath = join(monthPath, day);
|
|
36
|
+
if (!statSync(dayPath).isDirectory()) continue;
|
|
37
|
+
for (const file of readdirSync(dayPath)) {
|
|
38
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
39
|
+
const fullPath = join(dayPath, file);
|
|
40
|
+
try {
|
|
41
|
+
const stat = statSync(fullPath);
|
|
42
|
+
const sessionId = basename(file, ".jsonl");
|
|
43
|
+
if (stat.mtimeMs < cutoff && !hookManagedIds?.has(sessionId)) continue;
|
|
44
|
+
results.push({ sessionId, path: fullPath, lastModified: stat.mtimeMs, size: stat.size });
|
|
45
|
+
} catch {
|
|
46
|
+
// Skip unreadable files
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// sessions dir not accessible
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Archived sessions: archived_sessions/*.jsonl
|
|
58
|
+
const archivedDir = join(codexDir, "archived_sessions");
|
|
59
|
+
if (existsSync(archivedDir)) {
|
|
60
|
+
try {
|
|
61
|
+
for (const file of readdirSync(archivedDir)) {
|
|
62
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
63
|
+
const fullPath = join(archivedDir, file);
|
|
64
|
+
try {
|
|
65
|
+
const stat = statSync(fullPath);
|
|
66
|
+
const sessionId = basename(file, ".jsonl");
|
|
67
|
+
if (stat.mtimeMs < cutoff && !hookManagedIds?.has(sessionId)) continue;
|
|
68
|
+
results.push({ sessionId, path: fullPath, lastModified: stat.mtimeMs, size: stat.size });
|
|
69
|
+
} catch {
|
|
70
|
+
// Skip unreadable files
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// archived dir not accessible
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getCodexProcessPids(logger: Logger, timeoutMs?: number, processes?: { pid: number; command: string }[]): number[] {
|
|
82
|
+
const result = maybeRunCommand(["pgrep", "-f", "codex"], timeoutMs);
|
|
83
|
+
if (result.ok) {
|
|
84
|
+
return result.output
|
|
85
|
+
.split(/\r?\n/)
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.map((s) => Number.parseInt(s, 10))
|
|
88
|
+
.filter((n) => !Number.isNaN(n));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
logger.debug("codex", "pgrep failed — falling back to process collector data", { error: result.error });
|
|
92
|
+
|
|
93
|
+
if (processes && processes.length > 0) {
|
|
94
|
+
return processes
|
|
95
|
+
.filter((p) => isCodexProcessCommand(p.command))
|
|
96
|
+
.map((p) => p.pid);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isCodexProcessCommand(command: string): boolean {
|
|
103
|
+
return /(?:^|[\\/])codex(?:\.exe)?(?:\s|$)/i.test(command) && !/(scrylog|hook)/i.test(command);
|
|
104
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { SessionModel, SessionState, CodexSessionMeta } from "../../../shared/types";
|
|
4
|
+
|
|
5
|
+
export interface CodexHookFileData {
|
|
6
|
+
eventType?: string;
|
|
7
|
+
eventTime?: number;
|
|
8
|
+
hookSeq?: number;
|
|
9
|
+
state?: SessionState;
|
|
10
|
+
hasAttention?: boolean;
|
|
11
|
+
attentionKind?: "permission" | "question" | "turn" | "none";
|
|
12
|
+
summary?: string;
|
|
13
|
+
activeTool?: string;
|
|
14
|
+
toolName?: string;
|
|
15
|
+
toolInputSummary?: string;
|
|
16
|
+
toolResultStatus?: "started" | "completed" | "error" | "blocked" | "denied" | "unknown";
|
|
17
|
+
permissionMode?: string;
|
|
18
|
+
startSource?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
endReason?: string;
|
|
21
|
+
transcriptPath?: string;
|
|
22
|
+
currentPath?: string;
|
|
23
|
+
turnId?: string;
|
|
24
|
+
agentId?: string;
|
|
25
|
+
agentType?: string;
|
|
26
|
+
lastActivity?: number;
|
|
27
|
+
parentSessionId?: string;
|
|
28
|
+
detectionSource?: "codex-hook";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isValidHookData(data: CodexHookFileData): boolean {
|
|
32
|
+
return typeof data.eventType === "string"
|
|
33
|
+
&& data.eventType.length > 0
|
|
34
|
+
&& typeof data.eventTime === "number"
|
|
35
|
+
&& Number.isFinite(data.eventTime)
|
|
36
|
+
&& typeof data.hookSeq === "number"
|
|
37
|
+
&& Number.isFinite(data.hookSeq)
|
|
38
|
+
&& data.state !== undefined
|
|
39
|
+
&& typeof data.hasAttention === "boolean"
|
|
40
|
+
&& typeof data.attentionKind === "string";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readHookData(hooksDir: string, sessionId: string): Partial<SessionModel> | null {
|
|
44
|
+
if (!existsSync(hooksDir)) return null;
|
|
45
|
+
const hookFile = join(hooksDir, `${sessionId}.json`);
|
|
46
|
+
if (!existsSync(hookFile)) return null;
|
|
47
|
+
try {
|
|
48
|
+
const raw = readFileSync(hookFile, "utf8");
|
|
49
|
+
const data = JSON.parse(raw) as CodexHookFileData;
|
|
50
|
+
if (!isValidHookData(data)) return null;
|
|
51
|
+
return {
|
|
52
|
+
...(data.state !== undefined ? { state: data.state } : {}),
|
|
53
|
+
...(data.hasAttention !== undefined ? { hasAttention: data.hasAttention } : {}),
|
|
54
|
+
...(data.summary !== undefined ? { summary: data.summary } : {}),
|
|
55
|
+
...(data.activeTool !== undefined ? { activeTool: data.activeTool } : {}),
|
|
56
|
+
...(data.currentPath !== undefined ? { currentPath: data.currentPath } : {}),
|
|
57
|
+
...(data.lastActivity !== undefined ? { lastActivity: data.lastActivity } : {}),
|
|
58
|
+
...(data.parentSessionId !== undefined ? { parentSessionId: data.parentSessionId } : {}),
|
|
59
|
+
...(data.detectionSource !== undefined ? { detectionSource: data.detectionSource } : {}),
|
|
60
|
+
codex: {
|
|
61
|
+
sessionId,
|
|
62
|
+
transcriptPath: data.transcriptPath ?? "",
|
|
63
|
+
...(data.eventType !== undefined ? { hookEventType: data.eventType } : {}),
|
|
64
|
+
...(data.eventTime !== undefined ? { hookEventTime: data.eventTime } : {}),
|
|
65
|
+
...(data.hookSeq !== undefined ? { hookSeq: data.hookSeq } : {}),
|
|
66
|
+
...(data.permissionMode !== undefined ? { permissionMode: data.permissionMode } : {}),
|
|
67
|
+
...(data.startSource !== undefined ? { startSource: data.startSource } : {}),
|
|
68
|
+
...(data.model !== undefined ? { model: data.model } : {}),
|
|
69
|
+
...(data.endReason !== undefined ? { endReason: data.endReason } : {}),
|
|
70
|
+
...(data.toolName !== undefined ? { toolName: data.toolName } : {}),
|
|
71
|
+
...(data.toolInputSummary !== undefined ? { toolInputSummary: data.toolInputSummary } : {}),
|
|
72
|
+
...(data.toolResultStatus !== undefined ? { toolResultStatus: data.toolResultStatus } : {}),
|
|
73
|
+
...(data.agentId !== undefined ? { agentId: data.agentId } : {}),
|
|
74
|
+
...(data.agentType !== undefined ? { agentType: data.agentType } : {}),
|
|
75
|
+
...(data.turnId !== undefined ? { turnId: data.turnId } : {}),
|
|
76
|
+
...(data.parentSessionId !== undefined ? { parentSessionId: data.parentSessionId } : {}),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isHookNewer(existing: SessionModel | undefined, hookData: Partial<SessionModel>): boolean {
|
|
85
|
+
const incomingSeq = hookData.codex?.hookSeq;
|
|
86
|
+
const existingSeq = existing?.codex?.hookSeq;
|
|
87
|
+
if (typeof incomingSeq === "number" && typeof existingSeq === "number") {
|
|
88
|
+
return incomingSeq >= existingSeq;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const incomingTime = hookData.codex?.hookEventTime ?? hookData.lastActivity ?? 0;
|
|
92
|
+
const existingTime = existing?.codex?.hookEventTime ?? existing?.lastActivity ?? 0;
|
|
93
|
+
return incomingTime >= existingTime;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getHookTime(session: Partial<SessionModel> | undefined): number {
|
|
97
|
+
return session?.codex?.hookEventTime ?? session?.lastActivity ?? 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getExistingHookData(existing: SessionModel | undefined): Partial<SessionModel> | null {
|
|
101
|
+
if (!existing) return null;
|
|
102
|
+
if (existing.detectionSource !== "codex-hook") return null;
|
|
103
|
+
if (!existing.codex?.hookEventType || typeof existing.codex.hookEventTime !== "number" || typeof existing.codex.hookSeq !== "number") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
state: existing.state,
|
|
109
|
+
hasAttention: existing.hasAttention,
|
|
110
|
+
summary: existing.summary,
|
|
111
|
+
activeTool: existing.activeTool,
|
|
112
|
+
currentPath: existing.currentPath,
|
|
113
|
+
lastActivity: existing.lastActivity,
|
|
114
|
+
parentSessionId: existing.parentSessionId,
|
|
115
|
+
detectionSource: "codex-hook",
|
|
116
|
+
codex: {
|
|
117
|
+
sessionId: existing.codex.sessionId,
|
|
118
|
+
transcriptPath: existing.codex.transcriptPath,
|
|
119
|
+
hookEventType: existing.codex.hookEventType,
|
|
120
|
+
hookEventTime: existing.codex.hookEventTime,
|
|
121
|
+
hookSeq: existing.codex.hookSeq,
|
|
122
|
+
...(existing.codex.permissionMode ? { permissionMode: existing.codex.permissionMode } : {}),
|
|
123
|
+
...(existing.codex.startSource ? { startSource: existing.codex.startSource } : {}),
|
|
124
|
+
...(existing.codex.model ? { model: existing.codex.model } : {}),
|
|
125
|
+
...(existing.codex.endReason ? { endReason: existing.codex.endReason } : {}),
|
|
126
|
+
...(existing.codex.toolName ? { toolName: existing.codex.toolName } : {}),
|
|
127
|
+
...(existing.codex.toolInputSummary ? { toolInputSummary: existing.codex.toolInputSummary } : {}),
|
|
128
|
+
...(existing.codex.toolResultStatus ? { toolResultStatus: existing.codex.toolResultStatus } : {}),
|
|
129
|
+
...(existing.codex.agentId ? { agentId: existing.codex.agentId } : {}),
|
|
130
|
+
...(existing.codex.agentType ? { agentType: existing.codex.agentType } : {}),
|
|
131
|
+
...(existing.codex.parentSessionId ? { parentSessionId: existing.codex.parentSessionId } : {}),
|
|
132
|
+
...(existing.codex.forkedFromId ? { forkedFromId: existing.codex.forkedFromId } : {}),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const MERGEABLE_FIELDS: (keyof CodexSessionMeta)[] = [
|
|
138
|
+
"hookEventType", "hookEventTime", "hookSeq",
|
|
139
|
+
"permissionMode", "startSource", "endReason",
|
|
140
|
+
"toolName", "toolInputSummary", "toolResultStatus",
|
|
141
|
+
"model", "parentSessionId", "forkedFromId",
|
|
142
|
+
"turnId", "attentionKind", "agentId", "agentType", "agentNickname", "agentRole",
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
export function mergeCodexMeta(
|
|
146
|
+
base: CodexSessionMeta,
|
|
147
|
+
incoming: Partial<CodexSessionMeta>,
|
|
148
|
+
): CodexSessionMeta {
|
|
149
|
+
const result = { ...base };
|
|
150
|
+
for (const key of MERGEABLE_FIELDS) {
|
|
151
|
+
if (incoming[key] !== undefined) {
|
|
152
|
+
(result as Record<string, unknown>)[key] = incoming[key];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function mergeHookIntoModel(model: SessionModel, hookData: Partial<SessionModel>, transcriptPath: string): SessionModel {
|
|
159
|
+
const hookCodex = hookData.codex ?? { sessionId: model.codex?.sessionId ?? model.id.replace(/^codex:/, ""), transcriptPath };
|
|
160
|
+
return {
|
|
161
|
+
...model,
|
|
162
|
+
...hookData,
|
|
163
|
+
state: (hookData.state as SessionState | undefined) ?? model.state,
|
|
164
|
+
hasAttention: hookData.hasAttention ?? model.hasAttention,
|
|
165
|
+
id: model.id,
|
|
166
|
+
source: model.source,
|
|
167
|
+
summary: hookData.summary ?? model.summary,
|
|
168
|
+
lastActivity: typeof hookData.lastActivity === "number" ? hookData.lastActivity : model.lastActivity,
|
|
169
|
+
activeTool: hookData.activeTool ?? model.activeTool,
|
|
170
|
+
detectionSource: "codex-hook",
|
|
171
|
+
codex: mergeCodexMeta(
|
|
172
|
+
{
|
|
173
|
+
sessionId: hookCodex.sessionId ?? model.codex?.sessionId ?? model.id.replace(/^codex:/, ""),
|
|
174
|
+
transcriptPath: hookCodex.transcriptPath || transcriptPath,
|
|
175
|
+
},
|
|
176
|
+
hookCodex,
|
|
177
|
+
),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CodexCollector } from "./collector";
|
|
2
|
+
export type { CodexHookFileData } from "./hooks";
|
|
3
|
+
|
|
4
|
+
// Test-accessible pure functions (not part of collector public API,
|
|
5
|
+
// but exported for direct unit testing without replicating logic)
|
|
6
|
+
export { extractMeta } from "./transcript";
|
|
7
|
+
export { mergeCodexMeta } from "./hooks";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface CodexJsonlEntry {
|
|
4
|
+
type?: string;
|
|
5
|
+
timestamp?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
payload?: Record<string, unknown>;
|
|
8
|
+
session_id?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CodexResponseItem extends CodexJsonlEntry {
|
|
14
|
+
type: "response_item";
|
|
15
|
+
payload: {
|
|
16
|
+
type: string;
|
|
17
|
+
role?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
content?: unknown;
|
|
20
|
+
phase?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CodexEventMsg extends CodexJsonlEntry {
|
|
26
|
+
type: "event_msg";
|
|
27
|
+
payload: {
|
|
28
|
+
type: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isResponseItem(entry: CodexJsonlEntry): entry is CodexResponseItem {
|
|
35
|
+
return entry.type === "response_item";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isEventMsg(entry: CodexJsonlEntry): entry is CodexEventMsg {
|
|
39
|
+
return entry.type === "event_msg";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getResponseItemText(item: CodexResponseItem): string | undefined {
|
|
43
|
+
const content = item.payload.content;
|
|
44
|
+
if (!Array.isArray(content)) return undefined;
|
|
45
|
+
for (const block of content) {
|
|
46
|
+
if (
|
|
47
|
+
typeof block === "object" && block !== null
|
|
48
|
+
&& ("type" in block)
|
|
49
|
+
&& (
|
|
50
|
+
(block as { type: string }).type === "output_text"
|
|
51
|
+
|| (block as { type: string }).type === "input_text"
|
|
52
|
+
)
|
|
53
|
+
&& typeof (block as { text?: string }).text === "string"
|
|
54
|
+
) {
|
|
55
|
+
return (block as { text: string }).text;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getResponseItemRole(item: CodexResponseItem): string | undefined {
|
|
62
|
+
return item.payload.role;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getFunctionCallName(item: CodexResponseItem): string | undefined {
|
|
66
|
+
if (item.payload.type !== "function_call") return undefined;
|
|
67
|
+
return typeof item.payload.name === "string" ? item.payload.name : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseJsonlFile(path: string, maxBytes?: number): CodexJsonlEntry[] {
|
|
71
|
+
try {
|
|
72
|
+
const content = readTextTail(path, maxBytes);
|
|
73
|
+
return parseJsonlContent(content);
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseJsonlContent(content: string): CodexJsonlEntry[] {
|
|
80
|
+
return content
|
|
81
|
+
.split(/\r?\n/)
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.map((line) => {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(line) as CodexJsonlEntry;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.filter((entry): entry is CodexJsonlEntry => entry !== null);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readTextTail(path: string, maxBytes?: number): string {
|
|
94
|
+
if (!maxBytes || maxBytes <= 0) return readFileSync(path, "utf8");
|
|
95
|
+
const stat = statSync(path);
|
|
96
|
+
if (stat.size <= maxBytes) return readFileSync(path, "utf8");
|
|
97
|
+
|
|
98
|
+
const fd = openSync(path, "r");
|
|
99
|
+
try {
|
|
100
|
+
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
101
|
+
readSync(fd, buffer, 0, maxBytes, stat.size - maxBytes);
|
|
102
|
+
const text = buffer.toString("utf8");
|
|
103
|
+
const firstNewline = text.indexOf("\n");
|
|
104
|
+
return firstNewline >= 0 ? text.slice(firstNewline + 1) : text;
|
|
105
|
+
} finally {
|
|
106
|
+
closeSync(fd);
|
|
107
|
+
}
|
|
108
|
+
}
|