@seanxdo/superview 0.1.13
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/README.md +193 -0
- package/README.zh-CN.md +193 -0
- package/core/contextReplay.ts +388 -0
- package/core/cost.ts +125 -0
- package/core/hash.ts +5 -0
- package/core/history.ts +96 -0
- package/core/id.ts +6 -0
- package/core/normalizer.ts +720 -0
- package/core/parser.ts +53 -0
- package/core/redactor.ts +49 -0
- package/core/replay.ts +55 -0
- package/core/timeline.ts +350 -0
- package/core/types.ts +460 -0
- package/dist/ui/assets/index-BUbbOxsU.js +18 -0
- package/dist/ui/assets/index-DafedT5l.css +1 -0
- package/dist/ui/index.html +13 -0
- package/package.json +72 -0
- package/runtime-node/adapters/claude-code.ts +205 -0
- package/runtime-node/adapters/codex.ts +24 -0
- package/runtime-node/adapters/index.ts +18 -0
- package/runtime-node/adapters/opencode.ts +193 -0
- package/runtime-node/adapters/shared.ts +113 -0
- package/runtime-node/cli-ingest.ts +7 -0
- package/runtime-node/cli-start.js +15 -0
- package/runtime-node/cli-start.ts +9 -0
- package/runtime-node/dev-server.ts +6 -0
- package/runtime-node/git-provider.ts +102 -0
- package/runtime-node/history.ts +9 -0
- package/runtime-node/ingest-worker.ts +32 -0
- package/runtime-node/ingest.ts +362 -0
- package/runtime-node/prod-server.ts +24 -0
- package/runtime-node/scanner.ts +13 -0
- package/runtime-node/server.ts +183 -0
- package/storage/database.ts +1016 -0
- package/storage/paths.ts +20 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { AgentLogAdapter, AgentLogSource, NormalizedBundle, ParsedAgentEvent, TokenUsage } from "../../core/types";
|
|
7
|
+
import { normalizeCodexLines } from "../../core/normalizer";
|
|
8
|
+
import { asRecord, makeTokenUsage, numberTimestamp, parsedEvent, readJsonFile, stringValue } from "./shared";
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
export const opencodeAdapter: AgentLogAdapter = {
|
|
13
|
+
provider: "opencode",
|
|
14
|
+
async scan(config) {
|
|
15
|
+
if (config?.path) {
|
|
16
|
+
return [await fileLikeSource(config.path)];
|
|
17
|
+
}
|
|
18
|
+
const stdout = await runOpencode(["session", "list", "--format", "json"]);
|
|
19
|
+
const sessions = stdout.trim() ? asArray(JSON.parse(stdout)) : [];
|
|
20
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "superview-opencode-export-"));
|
|
21
|
+
const sources: AgentLogSource[] = [];
|
|
22
|
+
for (const session of sessions) {
|
|
23
|
+
const record = asRecord(session);
|
|
24
|
+
const id = stringValue(record.id) ?? stringValue(record.sessionID) ?? stringValue(record.sessionId);
|
|
25
|
+
if (!id) continue;
|
|
26
|
+
const exported = await runOpencode(["export", id, "--sanitize"]);
|
|
27
|
+
const exportPath = path.join(tempDir, `${id}.json`);
|
|
28
|
+
await writeFile(exportPath, exported, "utf8");
|
|
29
|
+
sources.push(await fileLikeSource(exportPath));
|
|
30
|
+
}
|
|
31
|
+
return sources;
|
|
32
|
+
},
|
|
33
|
+
async parseSource(source, options = {}) {
|
|
34
|
+
const json = await readJsonFile(source.path);
|
|
35
|
+
return normalizeOpenCodeExport(json, source.path, options.repoRoot);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function normalizeOpenCodeExport(json: unknown, sourcePath: string, repoRoot?: string | null): NormalizedBundle | null {
|
|
40
|
+
const root = asRecord(json);
|
|
41
|
+
const session = asRecord(root.session ?? root);
|
|
42
|
+
const messages = asArray(root.messages ?? root.message ?? root.parts);
|
|
43
|
+
const externalSessionId = stringValue(session.id) ?? path.basename(sourcePath, ".json");
|
|
44
|
+
const cwd = stringValue(session.cwd) ?? stringValue(root.cwd) ?? process.cwd();
|
|
45
|
+
const startedAt = timestampFromValue(asRecord(session.time).created ?? session.created ?? messages[0]);
|
|
46
|
+
const version = stringValue(session.version) ?? stringValue(root.version);
|
|
47
|
+
const lines: ParsedAgentEvent[] = [
|
|
48
|
+
parsedEvent({
|
|
49
|
+
provider: "opencode",
|
|
50
|
+
sourcePath,
|
|
51
|
+
lineNo: 1,
|
|
52
|
+
timestamp: startedAt,
|
|
53
|
+
type: "session_meta",
|
|
54
|
+
payload: {
|
|
55
|
+
id: externalSessionId,
|
|
56
|
+
timestamp: startedAt,
|
|
57
|
+
cwd,
|
|
58
|
+
cli_version: version,
|
|
59
|
+
model_provider: stringValue(session.provider) ?? null,
|
|
60
|
+
source: "opencode"
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
let lineNo = 2;
|
|
66
|
+
for (const messageValue of messages) {
|
|
67
|
+
const message = asRecord(messageValue);
|
|
68
|
+
const timestamp = timestampFromValue(asRecord(message.time).created ?? message.created ?? message.timestamp);
|
|
69
|
+
const role = stringValue(message.role) ?? stringValue(message.type);
|
|
70
|
+
const parts = asArray(message.parts ?? message.content);
|
|
71
|
+
const usage = makeTokenUsage(message.tokens ?? message.usage);
|
|
72
|
+
|
|
73
|
+
const messageText = textFromParts(parts.length > 0 ? parts : message.content);
|
|
74
|
+
if ((role === "user" || role === "assistant") && messageText) {
|
|
75
|
+
lines.push(
|
|
76
|
+
parsedEvent({
|
|
77
|
+
provider: "opencode",
|
|
78
|
+
sourcePath,
|
|
79
|
+
lineNo,
|
|
80
|
+
timestamp,
|
|
81
|
+
type: "response_item",
|
|
82
|
+
payload: {
|
|
83
|
+
type: "message",
|
|
84
|
+
role,
|
|
85
|
+
content: [{ type: role === "assistant" ? "output_text" : "input_text", text: messageText }],
|
|
86
|
+
...(usage ? { usage } : {})
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
lineNo += 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
const partRecord = asRecord(part);
|
|
95
|
+
if (!isToolPart(partRecord)) continue;
|
|
96
|
+
lines.push(
|
|
97
|
+
parsedEvent({
|
|
98
|
+
provider: "opencode",
|
|
99
|
+
sourcePath,
|
|
100
|
+
lineNo,
|
|
101
|
+
timestamp,
|
|
102
|
+
type: "response_item",
|
|
103
|
+
payload: {
|
|
104
|
+
type: "function_call",
|
|
105
|
+
call_id: stringValue(partRecord.id) ?? stringValue(partRecord.callID) ?? stringValue(partRecord.callId),
|
|
106
|
+
name: stringValue(partRecord.tool) ?? stringValue(partRecord.name) ?? "tool",
|
|
107
|
+
arguments: JSON.stringify(partRecord.input ?? partRecord.arguments ?? {})
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
lineNo += 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (role === "tool") {
|
|
115
|
+
lines.push(
|
|
116
|
+
parsedEvent({
|
|
117
|
+
provider: "opencode",
|
|
118
|
+
sourcePath,
|
|
119
|
+
lineNo,
|
|
120
|
+
timestamp,
|
|
121
|
+
type: "response_item",
|
|
122
|
+
payload: {
|
|
123
|
+
type: "function_call_output",
|
|
124
|
+
call_id: stringValue(message.toolCallId) ?? stringValue(message.tool_call_id) ?? stringValue(message.callId),
|
|
125
|
+
output: messageText || JSON.stringify(message)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
lineNo += 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return normalizeCodexLines(lines, {
|
|
134
|
+
repoRoot,
|
|
135
|
+
provider: "opencode",
|
|
136
|
+
prefixSessionId: true,
|
|
137
|
+
modelProvider: stringValue(session.provider) ?? null,
|
|
138
|
+
source: "opencode",
|
|
139
|
+
agentName: "OpenCode"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function fileLikeSource(filePath: string): Promise<AgentLogSource> {
|
|
144
|
+
const content = await readFile(filePath, "utf8");
|
|
145
|
+
const stats = await stat(filePath);
|
|
146
|
+
return {
|
|
147
|
+
provider: "opencode",
|
|
148
|
+
id: `opencode:${filePath}`,
|
|
149
|
+
path: filePath,
|
|
150
|
+
sizeBytes: Buffer.byteLength(content),
|
|
151
|
+
mtimeMs: stats.mtimeMs
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function runOpencode(args: string[]): Promise<string> {
|
|
156
|
+
const { stdout } = await execFileAsync("opencode", args, {
|
|
157
|
+
maxBuffer: 50 * 1024 * 1024
|
|
158
|
+
});
|
|
159
|
+
return stdout;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function asArray(value: unknown): unknown[] {
|
|
163
|
+
if (Array.isArray(value)) return value;
|
|
164
|
+
if (!value || typeof value !== "object") return [];
|
|
165
|
+
const record = value as Record<string, unknown>;
|
|
166
|
+
for (const key of ["items", "data", "sessions", "messages"]) {
|
|
167
|
+
if (Array.isArray(record[key])) return record[key];
|
|
168
|
+
}
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function timestampFromValue(value: unknown): string {
|
|
173
|
+
if (typeof value === "string") return value;
|
|
174
|
+
const numeric = numberTimestamp(value);
|
|
175
|
+
return numeric ?? new Date(0).toISOString();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function textFromParts(value: unknown): string {
|
|
179
|
+
if (typeof value === "string") return value;
|
|
180
|
+
return asArray(value)
|
|
181
|
+
.map((part) => {
|
|
182
|
+
if (typeof part === "string") return part;
|
|
183
|
+
const record = asRecord(part);
|
|
184
|
+
return stringValue(record.text) ?? stringValue(record.content) ?? stringValue(record.output) ?? "";
|
|
185
|
+
})
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isToolPart(part: Record<string, unknown>) {
|
|
191
|
+
const type = stringValue(part.type);
|
|
192
|
+
return type === "tool" || type === "tool_call" || Boolean(part.tool ?? part.name);
|
|
193
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { AgentLogSource, AgentProvider, ParsedAgentEvent, TimelineEvent, TokenUsage } from "../../core/types";
|
|
3
|
+
import { redactValue } from "../../core/redactor";
|
|
4
|
+
import { sha256 } from "../../core/hash";
|
|
5
|
+
|
|
6
|
+
export async function fileSource(provider: AgentProvider, filePath: string): Promise<AgentLogSource> {
|
|
7
|
+
const stats = await stat(filePath);
|
|
8
|
+
return {
|
|
9
|
+
provider,
|
|
10
|
+
id: `${provider}:${filePath}`,
|
|
11
|
+
path: filePath,
|
|
12
|
+
sizeBytes: stats.size,
|
|
13
|
+
mtimeMs: stats.mtimeMs
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function readJsonFile(sourcePath: string): Promise<unknown> {
|
|
18
|
+
return JSON.parse(await readFile(sourcePath, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parsedEvent(input: {
|
|
22
|
+
provider: AgentProvider;
|
|
23
|
+
sourcePath: string;
|
|
24
|
+
lineNo: number;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
type: string;
|
|
27
|
+
payload: unknown;
|
|
28
|
+
raw?: string;
|
|
29
|
+
}): ParsedAgentEvent {
|
|
30
|
+
const raw = input.raw ?? JSON.stringify({ timestamp: input.timestamp, type: input.type, payload: input.payload });
|
|
31
|
+
return {
|
|
32
|
+
provider: input.provider,
|
|
33
|
+
sourcePath: input.sourcePath,
|
|
34
|
+
lineNo: input.lineNo,
|
|
35
|
+
timestamp: input.timestamp,
|
|
36
|
+
type: input.type,
|
|
37
|
+
payload: input.payload,
|
|
38
|
+
redactedPayload: redactValue(input.payload),
|
|
39
|
+
sha256: sha256(raw)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function makeTokenUsage(value: unknown): TokenUsage | null {
|
|
44
|
+
const rawInput = firstNumber(value, ["input_tokens", "prompt_tokens", "input", "prompt"]);
|
|
45
|
+
const output = firstNumber(value, ["output_tokens", "completion_tokens", "output", "completion"]);
|
|
46
|
+
const reasoning = firstNumber(value, ["reasoning_tokens", "reasoning", "reasoning_output_tokens"]);
|
|
47
|
+
// OpenAI/Codex style: cached_tokens / cached_input_tokens are ALREADY included in prompt_tokens/input_tokens.
|
|
48
|
+
// Anthropic style: cache_read_input_tokens and cache_creation_input_tokens are reported SEPARATELY from input_tokens.
|
|
49
|
+
const cachedRead = firstNumber(value, ["cached_input_tokens", "cached_tokens", "cache_read_input_tokens", "cached_input", "cache_read", "read"]);
|
|
50
|
+
const anthropicCacheRead = firstNumber(value, ["cache_read_input_tokens"]);
|
|
51
|
+
const anthropicCacheCreation = firstNumber(value, ["cache_creation_input_tokens"]);
|
|
52
|
+
const explicitTotal = firstNumber(value, ["total_tokens", "total"]);
|
|
53
|
+
const isAnthropicStyle = anthropicCacheRead !== null || anthropicCacheCreation !== null;
|
|
54
|
+
const input = isAnthropicStyle
|
|
55
|
+
? (rawInput ?? 0) + (anthropicCacheRead ?? 0) + (anthropicCacheCreation ?? 0)
|
|
56
|
+
: rawInput;
|
|
57
|
+
const cachedInput = isAnthropicStyle ? (anthropicCacheRead ?? 0) : cachedRead;
|
|
58
|
+
const total = explicitTotal ?? sumKnown(input, output);
|
|
59
|
+
if (rawInput === null && output === null && reasoning === null && cachedRead === null && total === null && !isAnthropicStyle) return null;
|
|
60
|
+
return {
|
|
61
|
+
input: input ?? 0,
|
|
62
|
+
output: output ?? 0,
|
|
63
|
+
reasoning: reasoning ?? 0,
|
|
64
|
+
cachedInput: cachedInput ?? 0,
|
|
65
|
+
total: total ?? 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function attachTokenUsage(event: TimelineEvent, usage: TokenUsage | null): TimelineEvent {
|
|
70
|
+
return usage ? { ...event, tokenUsage: usage } : event;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function asRecord(value: unknown): Record<string, unknown> {
|
|
74
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function stringValue(value: unknown): string | null {
|
|
78
|
+
return typeof value === "string" ? value : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function numberTimestamp(value: unknown): string | null {
|
|
82
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
83
|
+
return new Date(value).toISOString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function firstNumber(value: unknown, keys: string[]): number | null {
|
|
87
|
+
const matches = collectNumbers(value, new Set(keys.map(normalizeKey)));
|
|
88
|
+
return matches[0] ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function collectNumbers(value: unknown, keys: Set<string>): number[] {
|
|
92
|
+
if (!value || typeof value !== "object") return [];
|
|
93
|
+
if (Array.isArray(value)) return value.flatMap((item) => collectNumbers(item, keys));
|
|
94
|
+
const matches: number[] = [];
|
|
95
|
+
for (const [key, child] of Object.entries(value)) {
|
|
96
|
+
if (keys.has(normalizeKey(key)) && typeof child === "number" && Number.isFinite(child) && child >= 0) {
|
|
97
|
+
matches.push(Math.trunc(child));
|
|
98
|
+
}
|
|
99
|
+
if (child && typeof child === "object") {
|
|
100
|
+
matches.push(...collectNumbers(child, keys));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return matches;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeKey(value: string) {
|
|
107
|
+
return value.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sumKnown(...values: Array<number | null>) {
|
|
111
|
+
const known = values.filter((value): value is number => value !== null);
|
|
112
|
+
return known.length > 0 ? known.reduce((sum, value) => sum + value, 0) : null;
|
|
113
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SuperViewDatabase } from "../storage/database";
|
|
2
|
+
import { IngestService } from "./ingest";
|
|
3
|
+
|
|
4
|
+
const db = new SuperViewDatabase();
|
|
5
|
+
const service = new IngestService(db);
|
|
6
|
+
const result = service.start(process.argv[2] ? { codexHome: process.argv[2] } : {});
|
|
7
|
+
console.log(JSON.stringify({ jobId: result.job.id, alreadyRunning: result.alreadyRunning }, null, 2));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const tsxDir = path.dirname(require.resolve("tsx/package.json"));
|
|
9
|
+
const tsxPath = path.join(tsxDir, "dist", "cli.mjs");
|
|
10
|
+
const cliPath = fileURLToPath(new URL("./cli-start.ts", import.meta.url));
|
|
11
|
+
|
|
12
|
+
const child = spawn(process.execPath, [tsxPath, cliPath, ...process.argv.slice(2)], {
|
|
13
|
+
stdio: "inherit"
|
|
14
|
+
});
|
|
15
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { startProdServer } from "./prod-server.js";
|
|
2
|
+
|
|
3
|
+
const portArg = process.argv.find((arg) => arg.startsWith("--port="));
|
|
4
|
+
const dataDirArg = process.argv.find((arg) => arg.startsWith("--data-dir="));
|
|
5
|
+
|
|
6
|
+
if (portArg) process.env.SUPERVIEW_PORT = portArg.split("=")[1];
|
|
7
|
+
if (dataDirArg) process.env.SUPERVIEW_DATA_DIR = dataDirArg.split("=")[1];
|
|
8
|
+
|
|
9
|
+
startProdServer();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { GitCommitRecord } from "../core/types";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const GIT_TIMEOUT_MS = 3000;
|
|
7
|
+
|
|
8
|
+
export async function getRepoRoot(cwd: string): Promise<string | null> {
|
|
9
|
+
try {
|
|
10
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS });
|
|
11
|
+
return stdout.trim() || null;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function getCommits(repoRoot: string, from?: string | null, to?: string | null): Promise<GitCommitRecord[]> {
|
|
18
|
+
const args = [
|
|
19
|
+
"-C",
|
|
20
|
+
repoRoot,
|
|
21
|
+
"log",
|
|
22
|
+
"--date=iso-strict",
|
|
23
|
+
"--pretty=format:%x1e%H%x1f%h%x1f%an%x1f%ae%x1f%ad%x1f%s",
|
|
24
|
+
"--numstat"
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
if (from) {
|
|
28
|
+
args.push(`--since=${formatGitDateArg(from)}`);
|
|
29
|
+
}
|
|
30
|
+
if (to) {
|
|
31
|
+
args.push(`--until=${formatGitDateArg(to)}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
36
|
+
timeout: GIT_TIMEOUT_MS,
|
|
37
|
+
maxBuffer: 10 * 1024 * 1024
|
|
38
|
+
});
|
|
39
|
+
return parseGitLogNumstat(stdout, repoRoot);
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseGitLogNumstat(stdout: string, repoRoot: string): GitCommitRecord[] {
|
|
46
|
+
return stdout
|
|
47
|
+
.split("\x1e")
|
|
48
|
+
.map((entry) => entry.trim())
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map((entry) => parseGitCommitEntry(entry, repoRoot))
|
|
51
|
+
.filter((commit): commit is GitCommitRecord => commit !== null);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseGitCommitEntry(entry: string, repoRoot: string): GitCommitRecord | null {
|
|
55
|
+
const lines = entry.split(/\r?\n/).filter(Boolean);
|
|
56
|
+
const header = lines.shift();
|
|
57
|
+
if (!header) return null;
|
|
58
|
+
|
|
59
|
+
const [hash, shortHash, authorName, authorEmail, timestamp, subject] = header.split("\x1f");
|
|
60
|
+
if (!hash || !shortHash || !timestamp) return null;
|
|
61
|
+
|
|
62
|
+
let filesChanged = 0;
|
|
63
|
+
let insertions = 0;
|
|
64
|
+
let deletions = 0;
|
|
65
|
+
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
const [inserted, deleted] = line.split(/\t/);
|
|
68
|
+
if (inserted === undefined || deleted === undefined) continue;
|
|
69
|
+
|
|
70
|
+
filesChanged += 1;
|
|
71
|
+
insertions += parseNumstatCount(inserted);
|
|
72
|
+
deletions += parseNumstatCount(deleted);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id: hash,
|
|
77
|
+
projectId: repoRoot,
|
|
78
|
+
repoRoot,
|
|
79
|
+
hash,
|
|
80
|
+
shortHash,
|
|
81
|
+
authorName: authorName || null,
|
|
82
|
+
authorEmail: authorEmail || null,
|
|
83
|
+
timestamp,
|
|
84
|
+
subject: subject ?? "",
|
|
85
|
+
filesChanged,
|
|
86
|
+
insertions,
|
|
87
|
+
deletions
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseNumstatCount(value: string): number {
|
|
92
|
+
const parsed = Number.parseInt(value, 10);
|
|
93
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatGitDateArg(value: string): string {
|
|
97
|
+
const parsed = new Date(value);
|
|
98
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
return `@${Math.floor(parsed.getTime() / 1000)}`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { parseCodexHistoryJsonlContent, type CodexHistoryParseResult } from "../core/history";
|
|
5
|
+
|
|
6
|
+
export async function parseCodexHistoryJsonlFile(sourcePath = join(homedir(), ".codex", "history.jsonl")): Promise<CodexHistoryParseResult> {
|
|
7
|
+
const content = await readFile(sourcePath, "utf8");
|
|
8
|
+
return parseCodexHistoryJsonlContent(content, sourcePath);
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SuperViewDatabase } from "../storage/database";
|
|
2
|
+
import { parseIngestOptions, runIngestJob } from "./ingest";
|
|
3
|
+
|
|
4
|
+
const [jobId, encodedOptions] = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (!jobId) {
|
|
7
|
+
throw new Error("Missing ingest job id");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const db = new SuperViewDatabase();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
if (process.env.SUPERVIEW_TEST_INGEST_WORKER_FAIL === "1") {
|
|
14
|
+
markFailed(db, jobId, "Forced ingest worker failure");
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
} else {
|
|
17
|
+
await runIngestJob(db, jobId, parseIngestOptions(encodedOptions), { workerPid: process.pid });
|
|
18
|
+
}
|
|
19
|
+
} finally {
|
|
20
|
+
db.close();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function markFailed(database: SuperViewDatabase, id: string, message: string) {
|
|
24
|
+
const job = database.getJob(id);
|
|
25
|
+
if (!job) return;
|
|
26
|
+
job.status = "failed";
|
|
27
|
+
job.phase = "failed";
|
|
28
|
+
job.finishedAt = new Date().toISOString();
|
|
29
|
+
job.currentFile = null;
|
|
30
|
+
job.errors.push(message);
|
|
31
|
+
database.upsertJob(job);
|
|
32
|
+
}
|