opencode-replay 1.0.2 → 1.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/README.md +81 -3
- package/dist/assets/gist-preview.js +126 -0
- package/dist/index.js +1307 -215
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -164,7 +164,8 @@ function renderBasePage(options) {
|
|
|
164
164
|
assetsPath = "./assets",
|
|
165
165
|
headExtra = "",
|
|
166
166
|
bodyClass = "",
|
|
167
|
-
totalPages
|
|
167
|
+
totalPages,
|
|
168
|
+
gistMode = false
|
|
168
169
|
} = options;
|
|
169
170
|
const bodyAttrs = [];
|
|
170
171
|
if (bodyClass)
|
|
@@ -172,6 +173,8 @@ function renderBasePage(options) {
|
|
|
172
173
|
if (totalPages !== undefined)
|
|
173
174
|
bodyAttrs.push(`data-total-pages="${totalPages}"`);
|
|
174
175
|
const bodyAttrStr = bodyAttrs.length > 0 ? ` ${bodyAttrs.join(" ")}` : "";
|
|
176
|
+
const gistScript = gistMode ? `
|
|
177
|
+
<script src="${assetsPath}/gist-preview.js"></script>` : "";
|
|
175
178
|
return `<!DOCTYPE html>
|
|
176
179
|
<html lang="en">
|
|
177
180
|
<head>
|
|
@@ -191,7 +194,7 @@ function renderBasePage(options) {
|
|
|
191
194
|
</div>
|
|
192
195
|
<script src="${assetsPath}/theme.js"></script>
|
|
193
196
|
<script src="${assetsPath}/highlight.js"></script>
|
|
194
|
-
<script src="${assetsPath}/search.js"></script
|
|
197
|
+
<script src="${assetsPath}/search.js"></script>${gistScript}
|
|
195
198
|
</body>
|
|
196
199
|
</html>`;
|
|
197
200
|
}
|
|
@@ -411,7 +414,8 @@ function renderIndexPage(data) {
|
|
|
411
414
|
sessions,
|
|
412
415
|
isAllProjects = false,
|
|
413
416
|
breadcrumbs = [],
|
|
414
|
-
assetsPath = "./assets"
|
|
417
|
+
assetsPath = "./assets",
|
|
418
|
+
gistMode
|
|
415
419
|
} = data;
|
|
416
420
|
const header = renderHeader({
|
|
417
421
|
title,
|
|
@@ -432,7 +436,8 @@ ${footer}
|
|
|
432
436
|
title,
|
|
433
437
|
content,
|
|
434
438
|
assetsPath,
|
|
435
|
-
bodyClass: "index-page"
|
|
439
|
+
bodyClass: "index-page",
|
|
440
|
+
gistMode
|
|
436
441
|
});
|
|
437
442
|
}
|
|
438
443
|
// src/render/templates/session.ts
|
|
@@ -539,7 +544,7 @@ function renderPagination(pageCount) {
|
|
|
539
544
|
</nav>`;
|
|
540
545
|
}
|
|
541
546
|
function renderSessionPage(data) {
|
|
542
|
-
const { session, projectName, timeline, pageCount, assetsPath = "../../assets" } = data;
|
|
547
|
+
const { session, projectName, timeline, pageCount, assetsPath = "../../assets", gistMode } = data;
|
|
543
548
|
const breadcrumbs = [
|
|
544
549
|
{ label: projectName ?? "Sessions", href: "../../index.html" },
|
|
545
550
|
{ label: session.title }
|
|
@@ -567,7 +572,8 @@ ${footer}
|
|
|
567
572
|
content,
|
|
568
573
|
assetsPath,
|
|
569
574
|
bodyClass: "session-page",
|
|
570
|
-
totalPages: pageCount
|
|
575
|
+
totalPages: pageCount,
|
|
576
|
+
gistMode
|
|
571
577
|
});
|
|
572
578
|
}
|
|
573
579
|
// src/render/components/tools/bash.ts
|
|
@@ -1619,7 +1625,8 @@ function renderConversationPage(data) {
|
|
|
1619
1625
|
messages,
|
|
1620
1626
|
pageNumber,
|
|
1621
1627
|
totalPages,
|
|
1622
|
-
assetsPath = "../../assets"
|
|
1628
|
+
assetsPath = "../../assets",
|
|
1629
|
+
gistMode
|
|
1623
1630
|
} = data;
|
|
1624
1631
|
const breadcrumbs = [
|
|
1625
1632
|
{ label: projectName ?? "Sessions", href: "../../index.html" },
|
|
@@ -1648,7 +1655,8 @@ ${footer}
|
|
|
1648
1655
|
content,
|
|
1649
1656
|
assetsPath,
|
|
1650
1657
|
bodyClass: "conversation-page",
|
|
1651
|
-
totalPages
|
|
1658
|
+
totalPages,
|
|
1659
|
+
gistMode
|
|
1652
1660
|
});
|
|
1653
1661
|
}
|
|
1654
1662
|
// src/render/git-commits.ts
|
|
@@ -1824,33 +1832,8 @@ function detectRepoFromMessages(messages) {
|
|
|
1824
1832
|
return null;
|
|
1825
1833
|
}
|
|
1826
1834
|
|
|
1827
|
-
// src/render/
|
|
1835
|
+
// src/render/data.ts
|
|
1828
1836
|
var PROMPTS_PER_PAGE = 5;
|
|
1829
|
-
async function ensureDir(dir) {
|
|
1830
|
-
await mkdir(dir, { recursive: true });
|
|
1831
|
-
}
|
|
1832
|
-
async function writeHtml(filePath, content) {
|
|
1833
|
-
await ensureDir(dirname(filePath));
|
|
1834
|
-
await Bun.write(filePath, content);
|
|
1835
|
-
}
|
|
1836
|
-
function getAssetsSourceDir() {
|
|
1837
|
-
const prodAssetsDir = join2(import.meta.dir, "assets");
|
|
1838
|
-
const devAssetsDir = join2(import.meta.dir, "../assets");
|
|
1839
|
-
if (Bun.file(join2(prodAssetsDir, "styles.css")).size) {
|
|
1840
|
-
return prodAssetsDir;
|
|
1841
|
-
}
|
|
1842
|
-
return devAssetsDir;
|
|
1843
|
-
}
|
|
1844
|
-
async function copyAssets(outputDir) {
|
|
1845
|
-
const assetsDir = join2(outputDir, "assets");
|
|
1846
|
-
await ensureDir(assetsDir);
|
|
1847
|
-
const sourceDir = getAssetsSourceDir();
|
|
1848
|
-
await copyFile(join2(sourceDir, "styles.css"), join2(assetsDir, "styles.css"));
|
|
1849
|
-
await copyFile(join2(sourceDir, "prism.css"), join2(assetsDir, "prism.css"));
|
|
1850
|
-
await copyFile(join2(sourceDir, "theme.js"), join2(assetsDir, "theme.js"));
|
|
1851
|
-
await copyFile(join2(sourceDir, "highlight.js"), join2(assetsDir, "highlight.js"));
|
|
1852
|
-
await copyFile(join2(sourceDir, "search.js"), join2(assetsDir, "search.js"));
|
|
1853
|
-
}
|
|
1854
1837
|
function getFirstPrompt(messages) {
|
|
1855
1838
|
for (const msg of messages) {
|
|
1856
1839
|
if (msg.message.role === "user") {
|
|
@@ -1939,17 +1922,12 @@ function paginateMessages(messages) {
|
|
|
1939
1922
|
}
|
|
1940
1923
|
return pages;
|
|
1941
1924
|
}
|
|
1942
|
-
|
|
1943
|
-
const sessionDir = join2(outputDir, "sessions", session2.id);
|
|
1944
|
-
await ensureDir(sessionDir);
|
|
1945
|
-
const messages = await getMessagesWithParts(storagePath, session2.id);
|
|
1946
|
-
const messageCount = messages.length;
|
|
1947
|
-
const firstPrompt = getFirstPrompt(messages);
|
|
1948
|
-
const timeline = buildTimeline(messages, repo);
|
|
1925
|
+
function calculateSessionStats(messages) {
|
|
1949
1926
|
let totalTokensInput = 0;
|
|
1950
1927
|
let totalTokensOutput = 0;
|
|
1951
1928
|
let totalCost = 0;
|
|
1952
1929
|
let model;
|
|
1930
|
+
let userMessageCount = 0;
|
|
1953
1931
|
for (const msg of messages) {
|
|
1954
1932
|
if (msg.message.role === "assistant") {
|
|
1955
1933
|
const asst = msg.message;
|
|
@@ -1963,20 +1941,69 @@ async function generateSessionHtml(storagePath, outputDir, session2, projectName
|
|
|
1963
1941
|
if (!model && asst.modelID) {
|
|
1964
1942
|
model = asst.modelID;
|
|
1965
1943
|
}
|
|
1944
|
+
} else if (msg.message.role === "user") {
|
|
1945
|
+
userMessageCount++;
|
|
1966
1946
|
}
|
|
1967
1947
|
}
|
|
1948
|
+
const pageCount = userMessageCount > 0 ? Math.ceil(userMessageCount / PROMPTS_PER_PAGE) : 0;
|
|
1949
|
+
return {
|
|
1950
|
+
messageCount: messages.length,
|
|
1951
|
+
pageCount,
|
|
1952
|
+
totalTokensInput,
|
|
1953
|
+
totalTokensOutput,
|
|
1954
|
+
totalCost,
|
|
1955
|
+
model
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/render/html.ts
|
|
1960
|
+
async function ensureDir(dir) {
|
|
1961
|
+
await mkdir(dir, { recursive: true });
|
|
1962
|
+
}
|
|
1963
|
+
async function writeHtml(filePath, content) {
|
|
1964
|
+
await ensureDir(dirname(filePath));
|
|
1965
|
+
await Bun.write(filePath, content);
|
|
1966
|
+
}
|
|
1967
|
+
function getAssetsSourceDir() {
|
|
1968
|
+
const prodAssetsDir = join2(import.meta.dir, "assets");
|
|
1969
|
+
const devAssetsDir = join2(import.meta.dir, "../assets");
|
|
1970
|
+
if (Bun.file(join2(prodAssetsDir, "styles.css")).size) {
|
|
1971
|
+
return prodAssetsDir;
|
|
1972
|
+
}
|
|
1973
|
+
return devAssetsDir;
|
|
1974
|
+
}
|
|
1975
|
+
async function copyAssets(outputDir, gistMode) {
|
|
1976
|
+
const assetsDir = join2(outputDir, "assets");
|
|
1977
|
+
await ensureDir(assetsDir);
|
|
1978
|
+
const sourceDir = getAssetsSourceDir();
|
|
1979
|
+
await copyFile(join2(sourceDir, "styles.css"), join2(assetsDir, "styles.css"));
|
|
1980
|
+
await copyFile(join2(sourceDir, "prism.css"), join2(assetsDir, "prism.css"));
|
|
1981
|
+
await copyFile(join2(sourceDir, "theme.js"), join2(assetsDir, "theme.js"));
|
|
1982
|
+
await copyFile(join2(sourceDir, "highlight.js"), join2(assetsDir, "highlight.js"));
|
|
1983
|
+
await copyFile(join2(sourceDir, "search.js"), join2(assetsDir, "search.js"));
|
|
1984
|
+
if (gistMode) {
|
|
1985
|
+
await copyFile(join2(sourceDir, "gist-preview.js"), join2(assetsDir, "gist-preview.js"));
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
async function generateSessionHtml(storagePath, outputDir, session2, projectName, repo, includeJson, gistMode) {
|
|
1989
|
+
const sessionDir = join2(outputDir, "sessions", session2.id);
|
|
1990
|
+
await ensureDir(sessionDir);
|
|
1991
|
+
const messages = await getMessagesWithParts(storagePath, session2.id);
|
|
1992
|
+
const firstPrompt = getFirstPrompt(messages);
|
|
1993
|
+
const timeline = buildTimeline(messages, repo);
|
|
1994
|
+
const stats = calculateSessionStats(messages);
|
|
1968
1995
|
const pages = paginateMessages(messages);
|
|
1969
|
-
const pageCount = pages.length;
|
|
1970
1996
|
const sessionOverviewHtml = renderSessionPage({
|
|
1971
1997
|
session: session2,
|
|
1972
1998
|
projectName,
|
|
1973
1999
|
timeline,
|
|
1974
|
-
messageCount,
|
|
1975
|
-
totalTokens: totalTokensInput > 0 || totalTokensOutput > 0 ? { input: totalTokensInput, output: totalTokensOutput } : undefined,
|
|
1976
|
-
totalCost: totalCost > 0 ? totalCost : undefined,
|
|
1977
|
-
pageCount,
|
|
1978
|
-
model,
|
|
1979
|
-
assetsPath: "../../assets"
|
|
2000
|
+
messageCount: stats.messageCount,
|
|
2001
|
+
totalTokens: stats.totalTokensInput > 0 || stats.totalTokensOutput > 0 ? { input: stats.totalTokensInput, output: stats.totalTokensOutput } : undefined,
|
|
2002
|
+
totalCost: stats.totalCost > 0 ? stats.totalCost : undefined,
|
|
2003
|
+
pageCount: stats.pageCount,
|
|
2004
|
+
model: stats.model,
|
|
2005
|
+
assetsPath: "../../assets",
|
|
2006
|
+
gistMode
|
|
1980
2007
|
});
|
|
1981
2008
|
await writeHtml(join2(sessionDir, "index.html"), sessionOverviewHtml);
|
|
1982
2009
|
for (let i = 0;i < pages.length; i++) {
|
|
@@ -1987,8 +2014,9 @@ async function generateSessionHtml(storagePath, outputDir, session2, projectName
|
|
|
1987
2014
|
projectName,
|
|
1988
2015
|
messages: pageMessages,
|
|
1989
2016
|
pageNumber,
|
|
1990
|
-
totalPages: pageCount,
|
|
1991
|
-
assetsPath: "../../assets"
|
|
2017
|
+
totalPages: stats.pageCount,
|
|
2018
|
+
assetsPath: "../../assets",
|
|
2019
|
+
gistMode
|
|
1992
2020
|
});
|
|
1993
2021
|
const pageFile = `page-${String(pageNumber).padStart(3, "0")}.html`;
|
|
1994
2022
|
await writeHtml(join2(sessionDir, pageFile), pageHtml);
|
|
@@ -1998,23 +2026,16 @@ async function generateSessionHtml(storagePath, outputDir, session2, projectName
|
|
|
1998
2026
|
session: session2,
|
|
1999
2027
|
messages,
|
|
2000
2028
|
timeline,
|
|
2001
|
-
stats
|
|
2002
|
-
messageCount,
|
|
2003
|
-
pageCount,
|
|
2004
|
-
totalTokensInput,
|
|
2005
|
-
totalTokensOutput,
|
|
2006
|
-
totalCost,
|
|
2007
|
-
model
|
|
2008
|
-
}
|
|
2029
|
+
stats
|
|
2009
2030
|
};
|
|
2010
2031
|
await Bun.write(join2(sessionDir, "session.json"), JSON.stringify(jsonData, null, 2));
|
|
2011
2032
|
}
|
|
2012
|
-
return { messageCount, pageCount, firstPrompt };
|
|
2033
|
+
return { messageCount: stats.messageCount, pageCount: stats.pageCount, firstPrompt };
|
|
2013
2034
|
}
|
|
2014
2035
|
async function generateHtml(options) {
|
|
2015
|
-
const { storagePath, outputDir, all = false, sessionId, includeJson = false, onProgress, repo } = options;
|
|
2036
|
+
const { storagePath, outputDir, all = false, sessionId, includeJson = false, onProgress, repo, gistMode = false } = options;
|
|
2016
2037
|
await ensureDir(outputDir);
|
|
2017
|
-
await copyAssets(outputDir);
|
|
2038
|
+
await copyAssets(outputDir, gistMode);
|
|
2018
2039
|
const sessionCards = [];
|
|
2019
2040
|
let title = "OpenCode Sessions";
|
|
2020
2041
|
let projectName;
|
|
@@ -2027,7 +2048,7 @@ async function generateHtml(options) {
|
|
|
2027
2048
|
title: session2.title,
|
|
2028
2049
|
phase: "generating"
|
|
2029
2050
|
});
|
|
2030
|
-
const result = await generateSessionHtml(storagePath, outputDir, session2, project.name, repo, includeJson);
|
|
2051
|
+
const result = await generateSessionHtml(storagePath, outputDir, session2, project.name, repo, includeJson, gistMode);
|
|
2031
2052
|
totalPageCount += result.pageCount;
|
|
2032
2053
|
totalMessageCount += result.messageCount;
|
|
2033
2054
|
sessionCards.push({
|
|
@@ -2086,7 +2107,8 @@ async function generateHtml(options) {
|
|
|
2086
2107
|
subtitle: projectName,
|
|
2087
2108
|
sessions: sessionCards,
|
|
2088
2109
|
isAllProjects: all,
|
|
2089
|
-
assetsPath: "./assets"
|
|
2110
|
+
assetsPath: "./assets",
|
|
2111
|
+
gistMode
|
|
2090
2112
|
});
|
|
2091
2113
|
await writeHtml(join2(outputDir, "index.html"), indexHtml);
|
|
2092
2114
|
onProgress?.({
|
|
@@ -2102,154 +2124,981 @@ async function generateHtml(options) {
|
|
|
2102
2124
|
projectName
|
|
2103
2125
|
};
|
|
2104
2126
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
const
|
|
2110
|
-
const
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
const indexFile = Bun.file(indexPath);
|
|
2139
|
-
if (await indexFile.exists()) {
|
|
2140
|
-
file = indexFile;
|
|
2141
|
-
fileExists = true;
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
if (!fileExists) {
|
|
2145
|
-
return new Response("Not Found", {
|
|
2146
|
-
status: 404,
|
|
2147
|
-
headers: { "Content-Type": "text/plain" }
|
|
2148
|
-
});
|
|
2149
|
-
}
|
|
2150
|
-
const content = await file.arrayBuffer();
|
|
2151
|
-
const etag = `W/"${Bun.hash(new Uint8Array(content)).toString(16)}"`;
|
|
2152
|
-
const ifNoneMatch = req.headers.get("If-None-Match");
|
|
2153
|
-
if (ifNoneMatch === etag) {
|
|
2154
|
-
return new Response(null, {
|
|
2155
|
-
status: 304,
|
|
2156
|
-
headers: { ETag: etag }
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
const isHashed = /\.[a-f0-9]{8,}\.(js|css|png|jpg|jpeg|gif|svg|woff2?)$/i.test(targetPath);
|
|
2160
|
-
const responseHeaders = {
|
|
2161
|
-
"Content-Type": file.type,
|
|
2162
|
-
"Content-Length": String(content.byteLength),
|
|
2163
|
-
ETag: etag,
|
|
2164
|
-
"Cache-Control": isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600"
|
|
2165
|
-
};
|
|
2166
|
-
if (req.method === "HEAD") {
|
|
2167
|
-
return new Response(null, { headers: responseHeaders });
|
|
2168
|
-
}
|
|
2169
|
-
return new Response(content, { headers: responseHeaders });
|
|
2170
|
-
};
|
|
2171
|
-
}
|
|
2172
|
-
async function serve(options) {
|
|
2173
|
-
const { directory, port, open = true } = options;
|
|
2174
|
-
const handleRequest = createRequestHandler(directory);
|
|
2175
|
-
let server;
|
|
2176
|
-
try {
|
|
2177
|
-
server = Bun.serve({
|
|
2178
|
-
port,
|
|
2179
|
-
fetch: handleRequest,
|
|
2180
|
-
error(error) {
|
|
2181
|
-
console.error("Server error:", error);
|
|
2182
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
2183
|
-
}
|
|
2184
|
-
});
|
|
2185
|
-
} catch (err) {
|
|
2186
|
-
const error = err;
|
|
2187
|
-
if (error.code === "EADDRINUSE") {
|
|
2188
|
-
console.error(`Error: Port ${port} is already in use`);
|
|
2189
|
-
process.exit(1);
|
|
2127
|
+
// src/render/markdown/tools/bash.ts
|
|
2128
|
+
function renderBashToolMd(part) {
|
|
2129
|
+
const { state } = part;
|
|
2130
|
+
const input = state.input;
|
|
2131
|
+
const command = input?.command || "";
|
|
2132
|
+
const description = input?.description || state.title || "";
|
|
2133
|
+
const workdir = input?.workdir;
|
|
2134
|
+
const output = state.output || "";
|
|
2135
|
+
const error = state.error;
|
|
2136
|
+
const lines = [];
|
|
2137
|
+
const workdirText = workdir ? ` (in \`${workdir}\`)` : "";
|
|
2138
|
+
lines.push(`**Bash:** ${description}${workdirText}`);
|
|
2139
|
+
lines.push("");
|
|
2140
|
+
lines.push("```bash");
|
|
2141
|
+
lines.push(`$ ${command}`);
|
|
2142
|
+
lines.push("```");
|
|
2143
|
+
lines.push("");
|
|
2144
|
+
if (output) {
|
|
2145
|
+
const outputLines = output.split(`
|
|
2146
|
+
`).length;
|
|
2147
|
+
if (outputLines > 15) {
|
|
2148
|
+
lines.push("<details>");
|
|
2149
|
+
lines.push("<summary>Output (click to expand)</summary>");
|
|
2150
|
+
lines.push("");
|
|
2151
|
+
lines.push("```");
|
|
2152
|
+
lines.push(output);
|
|
2153
|
+
lines.push("```");
|
|
2154
|
+
lines.push("");
|
|
2155
|
+
lines.push("</details>");
|
|
2156
|
+
} else {
|
|
2157
|
+
lines.push("```");
|
|
2158
|
+
lines.push(output);
|
|
2159
|
+
lines.push("```");
|
|
2190
2160
|
}
|
|
2191
|
-
|
|
2161
|
+
lines.push("");
|
|
2192
2162
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2163
|
+
if (error) {
|
|
2164
|
+
lines.push(`> **Error:** ${error}`);
|
|
2165
|
+
lines.push("");
|
|
2166
|
+
}
|
|
2167
|
+
return lines.join(`
|
|
2197
2168
|
`);
|
|
2198
|
-
|
|
2199
|
-
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// src/render/markdown/tools/read.ts
|
|
2172
|
+
function renderReadToolMd(part) {
|
|
2173
|
+
const { state } = part;
|
|
2174
|
+
const input = state.input;
|
|
2175
|
+
const filePath = input?.filePath || state.title || "Unknown file";
|
|
2176
|
+
const offset = input?.offset;
|
|
2177
|
+
const limit = input?.limit;
|
|
2178
|
+
const output = state.output || "";
|
|
2179
|
+
const error = state.error;
|
|
2180
|
+
const lines = [];
|
|
2181
|
+
let rangeInfo = "";
|
|
2182
|
+
if (offset !== undefined || limit !== undefined) {
|
|
2183
|
+
const parts = [];
|
|
2184
|
+
if (offset !== undefined)
|
|
2185
|
+
parts.push(`from line ${offset}`);
|
|
2186
|
+
if (limit !== undefined)
|
|
2187
|
+
parts.push(`${limit} lines`);
|
|
2188
|
+
rangeInfo = ` (${parts.join(", ")})`;
|
|
2200
2189
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2190
|
+
lines.push(`**Read:** \`${filePath}\`${rangeInfo}`);
|
|
2191
|
+
lines.push("");
|
|
2192
|
+
if (output) {
|
|
2193
|
+
const contentLines = output.split(`
|
|
2194
|
+
`).length;
|
|
2195
|
+
if (contentLines > 30) {
|
|
2196
|
+
lines.push("<details>");
|
|
2197
|
+
lines.push(`<summary>Content (${contentLines} lines)</summary>`);
|
|
2198
|
+
lines.push("");
|
|
2199
|
+
lines.push("```");
|
|
2200
|
+
lines.push(output);
|
|
2201
|
+
lines.push("```");
|
|
2202
|
+
lines.push("");
|
|
2203
|
+
lines.push("</details>");
|
|
2204
|
+
} else {
|
|
2205
|
+
lines.push("```");
|
|
2206
|
+
lines.push(output);
|
|
2207
|
+
lines.push("```");
|
|
2208
|
+
}
|
|
2209
|
+
lines.push("");
|
|
2211
2210
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
}
|
|
2216
|
-
function openBrowser(url) {
|
|
2217
|
-
const platform = process.platform;
|
|
2218
|
-
if (platform === "darwin") {
|
|
2219
|
-
Bun.spawn(["open", url]);
|
|
2220
|
-
} else if (platform === "win32") {
|
|
2221
|
-
Bun.spawn(["cmd", "/c", "start", "", url]);
|
|
2222
|
-
} else {
|
|
2223
|
-
Bun.spawn(["xdg-open", url]);
|
|
2211
|
+
if (error) {
|
|
2212
|
+
lines.push(`> **Error:** ${error}`);
|
|
2213
|
+
lines.push("");
|
|
2224
2214
|
}
|
|
2215
|
+
return lines.join(`
|
|
2216
|
+
`);
|
|
2225
2217
|
}
|
|
2226
2218
|
|
|
2227
|
-
// src/
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2219
|
+
// src/render/markdown/tools/write.ts
|
|
2220
|
+
function renderWriteToolMd(part) {
|
|
2221
|
+
const { state } = part;
|
|
2222
|
+
const input = state.input;
|
|
2223
|
+
const filePath = input?.filePath || state.title || "Unknown file";
|
|
2224
|
+
const content = input?.content || "";
|
|
2225
|
+
const error = state.error;
|
|
2226
|
+
const status = state.status;
|
|
2227
|
+
const lines = [];
|
|
2228
|
+
const lineCount = content ? content.split(`
|
|
2229
|
+
`).length : 0;
|
|
2230
|
+
const sizeText = formatBytes(content.length);
|
|
2231
|
+
const statusText = status === "completed" ? "Created" : status === "error" ? "Failed" : "Writing...";
|
|
2232
|
+
lines.push(`**Write:** \`${filePath}\` - ${statusText} (${lineCount} lines, ${sizeText})`);
|
|
2233
|
+
lines.push("");
|
|
2234
|
+
if (content) {
|
|
2235
|
+
if (lineCount > 30) {
|
|
2236
|
+
lines.push("<details>");
|
|
2237
|
+
lines.push(`<summary>Content (${lineCount} lines)</summary>`);
|
|
2238
|
+
lines.push("");
|
|
2239
|
+
lines.push("```");
|
|
2240
|
+
lines.push(content);
|
|
2241
|
+
lines.push("```");
|
|
2242
|
+
lines.push("");
|
|
2243
|
+
lines.push("</details>");
|
|
2244
|
+
} else {
|
|
2245
|
+
lines.push("```");
|
|
2246
|
+
lines.push(content);
|
|
2247
|
+
lines.push("```");
|
|
2248
|
+
}
|
|
2249
|
+
lines.push("");
|
|
2240
2250
|
}
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
if (Bun.file(bunInstallPath).size) {
|
|
2245
|
-
return "bun";
|
|
2246
|
-
}
|
|
2247
|
-
} catch {}
|
|
2251
|
+
if (error) {
|
|
2252
|
+
lines.push(`> **Error:** ${error}`);
|
|
2253
|
+
lines.push("");
|
|
2248
2254
|
}
|
|
2249
|
-
return
|
|
2255
|
+
return lines.join(`
|
|
2256
|
+
`);
|
|
2250
2257
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2258
|
+
|
|
2259
|
+
// src/render/markdown/tools/edit.ts
|
|
2260
|
+
function renderEditToolMd(part) {
|
|
2261
|
+
const { state } = part;
|
|
2262
|
+
const input = state.input;
|
|
2263
|
+
const filePath = input?.filePath || state.title || "Unknown file";
|
|
2264
|
+
const oldString = input?.oldString || "";
|
|
2265
|
+
const newString = input?.newString || "";
|
|
2266
|
+
const replaceAll = input?.replaceAll || false;
|
|
2267
|
+
const error = state.error;
|
|
2268
|
+
const lines = [];
|
|
2269
|
+
const oldLines = oldString.split(`
|
|
2270
|
+
`).length;
|
|
2271
|
+
const newLines = newString.split(`
|
|
2272
|
+
`).length;
|
|
2273
|
+
const replaceAllText = replaceAll ? " (replace all)" : "";
|
|
2274
|
+
lines.push(`**Edit:** \`${filePath}\`${replaceAllText} - ${oldLines} lines -> ${newLines} lines`);
|
|
2275
|
+
lines.push("");
|
|
2276
|
+
lines.push("```diff");
|
|
2277
|
+
for (const line of oldString.split(`
|
|
2278
|
+
`)) {
|
|
2279
|
+
lines.push(`- ${line}`);
|
|
2280
|
+
}
|
|
2281
|
+
for (const line of newString.split(`
|
|
2282
|
+
`)) {
|
|
2283
|
+
lines.push(`+ ${line}`);
|
|
2284
|
+
}
|
|
2285
|
+
lines.push("```");
|
|
2286
|
+
lines.push("");
|
|
2287
|
+
if (error) {
|
|
2288
|
+
lines.push(`> **Error:** ${error}`);
|
|
2289
|
+
lines.push("");
|
|
2290
|
+
}
|
|
2291
|
+
return lines.join(`
|
|
2292
|
+
`);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// src/render/markdown/tools/glob.ts
|
|
2296
|
+
function renderGlobToolMd(part) {
|
|
2297
|
+
const { state } = part;
|
|
2298
|
+
const input = state.input;
|
|
2299
|
+
const pattern = input?.pattern || "";
|
|
2300
|
+
const searchPath = input?.path || ".";
|
|
2301
|
+
const output = state.output || "";
|
|
2302
|
+
const error = state.error;
|
|
2303
|
+
const lines = [];
|
|
2304
|
+
const files = output.trim() ? output.trim().split(`
|
|
2305
|
+
`).filter(Boolean) : [];
|
|
2306
|
+
const fileCount = files.length;
|
|
2307
|
+
lines.push(`**Glob:** \`${pattern}\` in \`${searchPath}\` - ${fileCount} files`);
|
|
2308
|
+
lines.push("");
|
|
2309
|
+
if (files.length > 0) {
|
|
2310
|
+
if (files.length > 20) {
|
|
2311
|
+
lines.push("<details>");
|
|
2312
|
+
lines.push(`<summary>Files (${fileCount})</summary>`);
|
|
2313
|
+
lines.push("");
|
|
2314
|
+
for (const file of files) {
|
|
2315
|
+
lines.push(`- \`${file}\``);
|
|
2316
|
+
}
|
|
2317
|
+
lines.push("");
|
|
2318
|
+
lines.push("</details>");
|
|
2319
|
+
} else {
|
|
2320
|
+
for (const file of files) {
|
|
2321
|
+
lines.push(`- \`${file}\``);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
lines.push("");
|
|
2325
|
+
} else if (!error) {
|
|
2326
|
+
lines.push("*No matching files*");
|
|
2327
|
+
lines.push("");
|
|
2328
|
+
}
|
|
2329
|
+
if (error) {
|
|
2330
|
+
lines.push(`> **Error:** ${error}`);
|
|
2331
|
+
lines.push("");
|
|
2332
|
+
}
|
|
2333
|
+
return lines.join(`
|
|
2334
|
+
`);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/render/markdown/tools/grep.ts
|
|
2338
|
+
function renderGrepToolMd(part) {
|
|
2339
|
+
const { state } = part;
|
|
2340
|
+
const input = state.input;
|
|
2341
|
+
const pattern = input?.pattern || "";
|
|
2342
|
+
const searchPath = input?.path || ".";
|
|
2343
|
+
const include = input?.include;
|
|
2344
|
+
const output = state.output || "";
|
|
2345
|
+
const error = state.error;
|
|
2346
|
+
const lines = [];
|
|
2347
|
+
const matches = parseGrepOutput2(output);
|
|
2348
|
+
const matchCount = matches.length;
|
|
2349
|
+
const includeText = include ? ` (filter: \`${include}\`)` : "";
|
|
2350
|
+
lines.push(`**Grep:** \`${pattern}\` in \`${searchPath}\`${includeText} - ${matchCount} matches`);
|
|
2351
|
+
lines.push("");
|
|
2352
|
+
if (matches.length > 0) {
|
|
2353
|
+
if (matches.length > 20) {
|
|
2354
|
+
lines.push("<details>");
|
|
2355
|
+
lines.push(`<summary>Matches (${matchCount})</summary>`);
|
|
2356
|
+
lines.push("");
|
|
2357
|
+
for (const match of matches) {
|
|
2358
|
+
lines.push(`- \`${match.file}:${match.line}\` ${match.content}`);
|
|
2359
|
+
}
|
|
2360
|
+
lines.push("");
|
|
2361
|
+
lines.push("</details>");
|
|
2362
|
+
} else {
|
|
2363
|
+
for (const match of matches) {
|
|
2364
|
+
lines.push(`- \`${match.file}:${match.line}\` ${match.content}`);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
lines.push("");
|
|
2368
|
+
} else if (!error) {
|
|
2369
|
+
lines.push("*No matches found*");
|
|
2370
|
+
lines.push("");
|
|
2371
|
+
}
|
|
2372
|
+
if (error) {
|
|
2373
|
+
lines.push(`> **Error:** ${error}`);
|
|
2374
|
+
lines.push("");
|
|
2375
|
+
}
|
|
2376
|
+
return lines.join(`
|
|
2377
|
+
`);
|
|
2378
|
+
}
|
|
2379
|
+
function parseGrepOutput2(output) {
|
|
2380
|
+
if (!output.trim())
|
|
2381
|
+
return [];
|
|
2382
|
+
const lines = output.trim().split(`
|
|
2383
|
+
`);
|
|
2384
|
+
const matches = [];
|
|
2385
|
+
for (const line of lines) {
|
|
2386
|
+
const match = line.match(/^(.+?):(\d+):?(.*)$/);
|
|
2387
|
+
if (match && match[1] && match[2]) {
|
|
2388
|
+
matches.push({
|
|
2389
|
+
file: match[1],
|
|
2390
|
+
line: parseInt(match[2], 10),
|
|
2391
|
+
content: match[3] || ""
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
return matches;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// src/render/markdown/tools/task.ts
|
|
2399
|
+
function renderTaskToolMd(part) {
|
|
2400
|
+
const { state } = part;
|
|
2401
|
+
const input = state.input;
|
|
2402
|
+
const description = input?.description || state.title || "Task";
|
|
2403
|
+
const prompt = input?.prompt || "";
|
|
2404
|
+
const agentType = input?.subagent_type || "general";
|
|
2405
|
+
const command = input?.command;
|
|
2406
|
+
const output = state.output || "";
|
|
2407
|
+
const error = state.error;
|
|
2408
|
+
const lines = [];
|
|
2409
|
+
lines.push(`**Task:** ${description} (\`${agentType}\` agent)`);
|
|
2410
|
+
lines.push("");
|
|
2411
|
+
if (command) {
|
|
2412
|
+
lines.push(`Command: \`${command}\``);
|
|
2413
|
+
lines.push("");
|
|
2414
|
+
}
|
|
2415
|
+
if (prompt) {
|
|
2416
|
+
const promptLines = prompt.split(`
|
|
2417
|
+
`).length;
|
|
2418
|
+
if (promptLines > 5) {
|
|
2419
|
+
lines.push("<details>");
|
|
2420
|
+
lines.push(`<summary>Prompt (${promptLines} lines)</summary>`);
|
|
2421
|
+
lines.push("");
|
|
2422
|
+
lines.push(prompt);
|
|
2423
|
+
lines.push("");
|
|
2424
|
+
lines.push("</details>");
|
|
2425
|
+
} else {
|
|
2426
|
+
lines.push("**Prompt:**");
|
|
2427
|
+
lines.push("> " + prompt.split(`
|
|
2428
|
+
`).join(`
|
|
2429
|
+
> `));
|
|
2430
|
+
}
|
|
2431
|
+
lines.push("");
|
|
2432
|
+
}
|
|
2433
|
+
if (output) {
|
|
2434
|
+
const resultLines = output.split(`
|
|
2435
|
+
`).length;
|
|
2436
|
+
if (resultLines > 15) {
|
|
2437
|
+
lines.push("<details>");
|
|
2438
|
+
lines.push(`<summary>Result (${resultLines} lines)</summary>`);
|
|
2439
|
+
lines.push("");
|
|
2440
|
+
lines.push(output);
|
|
2441
|
+
lines.push("");
|
|
2442
|
+
lines.push("</details>");
|
|
2443
|
+
} else {
|
|
2444
|
+
lines.push("**Result:**");
|
|
2445
|
+
lines.push(output);
|
|
2446
|
+
}
|
|
2447
|
+
lines.push("");
|
|
2448
|
+
}
|
|
2449
|
+
if (error) {
|
|
2450
|
+
lines.push(`> **Error:** ${error}`);
|
|
2451
|
+
lines.push("");
|
|
2452
|
+
}
|
|
2453
|
+
return lines.join(`
|
|
2454
|
+
`);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/render/markdown/tools/todowrite.ts
|
|
2458
|
+
function renderTodoWriteToolMd(part) {
|
|
2459
|
+
const { state } = part;
|
|
2460
|
+
const input = state.input;
|
|
2461
|
+
const todos = input?.todos || [];
|
|
2462
|
+
const error = state.error;
|
|
2463
|
+
const lines = [];
|
|
2464
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
2465
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
2466
|
+
const pending = todos.filter((t) => t.status === "pending").length;
|
|
2467
|
+
const cancelled = todos.filter((t) => t.status === "cancelled").length;
|
|
2468
|
+
lines.push(`**Todo List:** ${completed}/${todos.length} done`);
|
|
2469
|
+
if (inProgress > 0 || pending > 0 || cancelled > 0) {
|
|
2470
|
+
const stats = [];
|
|
2471
|
+
if (inProgress > 0)
|
|
2472
|
+
stats.push(`${inProgress} in progress`);
|
|
2473
|
+
if (pending > 0)
|
|
2474
|
+
stats.push(`${pending} pending`);
|
|
2475
|
+
if (cancelled > 0)
|
|
2476
|
+
stats.push(`${cancelled} cancelled`);
|
|
2477
|
+
lines.push(`*(${stats.join(", ")})*`);
|
|
2478
|
+
}
|
|
2479
|
+
lines.push("");
|
|
2480
|
+
if (todos.length > 0) {
|
|
2481
|
+
for (const todo of todos) {
|
|
2482
|
+
const checkbox = getStatusCheckbox(todo.status);
|
|
2483
|
+
const priorityBadge = todo.priority === "high" ? " **[HIGH]**" : todo.priority === "medium" ? " *[MEDIUM]*" : "";
|
|
2484
|
+
lines.push(`- ${checkbox} ${todo.content}${priorityBadge}`);
|
|
2485
|
+
}
|
|
2486
|
+
lines.push("");
|
|
2487
|
+
} else {
|
|
2488
|
+
lines.push("*No todos*");
|
|
2489
|
+
lines.push("");
|
|
2490
|
+
}
|
|
2491
|
+
if (error) {
|
|
2492
|
+
lines.push(`> **Error:** ${error}`);
|
|
2493
|
+
lines.push("");
|
|
2494
|
+
}
|
|
2495
|
+
return lines.join(`
|
|
2496
|
+
`);
|
|
2497
|
+
}
|
|
2498
|
+
function getStatusCheckbox(status) {
|
|
2499
|
+
switch (status) {
|
|
2500
|
+
case "completed":
|
|
2501
|
+
return "[x]";
|
|
2502
|
+
case "in_progress":
|
|
2503
|
+
return "[-]";
|
|
2504
|
+
case "pending":
|
|
2505
|
+
return "[ ]";
|
|
2506
|
+
case "cancelled":
|
|
2507
|
+
return "[~]";
|
|
2508
|
+
default:
|
|
2509
|
+
return "[ ]";
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// src/render/markdown/tools/webfetch.ts
|
|
2514
|
+
function renderWebFetchToolMd(part) {
|
|
2515
|
+
const { state } = part;
|
|
2516
|
+
const input = state.input;
|
|
2517
|
+
const url = input?.url || "";
|
|
2518
|
+
const format = input?.format || "markdown";
|
|
2519
|
+
const output = state.output || "";
|
|
2520
|
+
const error = state.error;
|
|
2521
|
+
const lines = [];
|
|
2522
|
+
const contentSize = formatBytes(output.length);
|
|
2523
|
+
lines.push(`**WebFetch:** [${truncateUrl2(url)}](${url}) - ${format} (${contentSize})`);
|
|
2524
|
+
lines.push("");
|
|
2525
|
+
if (output) {
|
|
2526
|
+
const contentLines = output.split(`
|
|
2527
|
+
`).length;
|
|
2528
|
+
if (contentLines > 30) {
|
|
2529
|
+
lines.push("<details>");
|
|
2530
|
+
lines.push(`<summary>Content (${contentLines} lines)</summary>`);
|
|
2531
|
+
lines.push("");
|
|
2532
|
+
lines.push(output);
|
|
2533
|
+
lines.push("");
|
|
2534
|
+
lines.push("</details>");
|
|
2535
|
+
} else {
|
|
2536
|
+
lines.push(output);
|
|
2537
|
+
}
|
|
2538
|
+
lines.push("");
|
|
2539
|
+
}
|
|
2540
|
+
if (error) {
|
|
2541
|
+
lines.push(`> **Error:** ${error}`);
|
|
2542
|
+
lines.push("");
|
|
2543
|
+
}
|
|
2544
|
+
return lines.join(`
|
|
2545
|
+
`);
|
|
2546
|
+
}
|
|
2547
|
+
function truncateUrl2(url, maxLength = 60) {
|
|
2548
|
+
if (url.length <= maxLength)
|
|
2549
|
+
return url;
|
|
2550
|
+
try {
|
|
2551
|
+
const parsed = new URL(url);
|
|
2552
|
+
const domain = parsed.hostname;
|
|
2553
|
+
const path = parsed.pathname;
|
|
2554
|
+
if (domain.length + path.length <= maxLength) {
|
|
2555
|
+
return domain + path;
|
|
2556
|
+
}
|
|
2557
|
+
const availableForPath = maxLength - domain.length - 3;
|
|
2558
|
+
if (availableForPath > 10) {
|
|
2559
|
+
return domain + path.slice(0, availableForPath) + "...";
|
|
2560
|
+
}
|
|
2561
|
+
return url.slice(0, maxLength - 3) + "...";
|
|
2562
|
+
} catch {
|
|
2563
|
+
return url.slice(0, maxLength - 3) + "...";
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// src/render/markdown/tools/batch.ts
|
|
2568
|
+
function renderBatchToolMd(part) {
|
|
2569
|
+
const { state } = part;
|
|
2570
|
+
const input = state.input;
|
|
2571
|
+
const toolCalls = input?.tool_calls || [];
|
|
2572
|
+
const output = state.output || "";
|
|
2573
|
+
const error = state.error;
|
|
2574
|
+
const lines = [];
|
|
2575
|
+
const toolCounts = new Map;
|
|
2576
|
+
for (const call of toolCalls) {
|
|
2577
|
+
const count = toolCounts.get(call.tool) || 0;
|
|
2578
|
+
toolCounts.set(call.tool, count + 1);
|
|
2579
|
+
}
|
|
2580
|
+
const toolSummary = Array.from(toolCounts.entries()).map(([tool, count]) => `${count} ${tool}`).join(", ");
|
|
2581
|
+
lines.push(`**Batch:** ${toolCalls.length} calls (${toolSummary})`);
|
|
2582
|
+
lines.push("");
|
|
2583
|
+
if (toolCalls.length > 0) {
|
|
2584
|
+
for (let i = 0;i < toolCalls.length; i++) {
|
|
2585
|
+
const call = toolCalls[i];
|
|
2586
|
+
const info = getToolInfo2(call.tool, call.parameters);
|
|
2587
|
+
lines.push(`${i + 1}. **${call.tool}**${info ? `: ${info}` : ""}`);
|
|
2588
|
+
}
|
|
2589
|
+
lines.push("");
|
|
2590
|
+
}
|
|
2591
|
+
if (output) {
|
|
2592
|
+
const outputLines = output.split(`
|
|
2593
|
+
`).length;
|
|
2594
|
+
if (outputLines > 30) {
|
|
2595
|
+
lines.push("<details>");
|
|
2596
|
+
lines.push(`<summary>Combined Output (${outputLines} lines)</summary>`);
|
|
2597
|
+
lines.push("");
|
|
2598
|
+
lines.push("```");
|
|
2599
|
+
lines.push(output);
|
|
2600
|
+
lines.push("```");
|
|
2601
|
+
lines.push("");
|
|
2602
|
+
lines.push("</details>");
|
|
2603
|
+
} else {
|
|
2604
|
+
lines.push("```");
|
|
2605
|
+
lines.push(output);
|
|
2606
|
+
lines.push("```");
|
|
2607
|
+
}
|
|
2608
|
+
lines.push("");
|
|
2609
|
+
}
|
|
2610
|
+
if (error) {
|
|
2611
|
+
lines.push(`> **Error:** ${error}`);
|
|
2612
|
+
lines.push("");
|
|
2613
|
+
}
|
|
2614
|
+
return lines.join(`
|
|
2615
|
+
`);
|
|
2616
|
+
}
|
|
2617
|
+
function getToolInfo2(tool, params) {
|
|
2618
|
+
switch (tool) {
|
|
2619
|
+
case "bash":
|
|
2620
|
+
return `\`${String(params.command || params.description || "")}\``;
|
|
2621
|
+
case "read":
|
|
2622
|
+
case "write":
|
|
2623
|
+
case "edit":
|
|
2624
|
+
return `\`${String(params.filePath || "")}\``;
|
|
2625
|
+
case "glob":
|
|
2626
|
+
case "grep":
|
|
2627
|
+
return `\`${String(params.pattern || "")}\``;
|
|
2628
|
+
case "webfetch":
|
|
2629
|
+
return String(params.url || "");
|
|
2630
|
+
case "task":
|
|
2631
|
+
return String(params.description || "");
|
|
2632
|
+
default:
|
|
2633
|
+
return "";
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/render/markdown/part.ts
|
|
2638
|
+
function renderTextPartMd(part) {
|
|
2639
|
+
if (part.ignored)
|
|
2640
|
+
return "";
|
|
2641
|
+
return part.text;
|
|
2642
|
+
}
|
|
2643
|
+
function renderReasoningPartMd(part) {
|
|
2644
|
+
const hasTiming = part.time?.start && part.time?.end;
|
|
2645
|
+
const durationText = hasTiming ? ` (${formatDuration(part.time.start, part.time.end)})` : "";
|
|
2646
|
+
return `<details>
|
|
2647
|
+
<summary>Thinking...${durationText}</summary>
|
|
2648
|
+
|
|
2649
|
+
${part.text}
|
|
2650
|
+
|
|
2651
|
+
</details>`;
|
|
2652
|
+
}
|
|
2653
|
+
function renderToolPartMd(part) {
|
|
2654
|
+
const { tool } = part;
|
|
2655
|
+
switch (tool) {
|
|
2656
|
+
case "bash":
|
|
2657
|
+
return renderBashToolMd(part);
|
|
2658
|
+
case "read":
|
|
2659
|
+
return renderReadToolMd(part);
|
|
2660
|
+
case "write":
|
|
2661
|
+
return renderWriteToolMd(part);
|
|
2662
|
+
case "edit":
|
|
2663
|
+
return renderEditToolMd(part);
|
|
2664
|
+
case "glob":
|
|
2665
|
+
return renderGlobToolMd(part);
|
|
2666
|
+
case "grep":
|
|
2667
|
+
return renderGrepToolMd(part);
|
|
2668
|
+
case "task":
|
|
2669
|
+
return renderTaskToolMd(part);
|
|
2670
|
+
case "todowrite":
|
|
2671
|
+
return renderTodoWriteToolMd(part);
|
|
2672
|
+
case "webfetch":
|
|
2673
|
+
return renderWebFetchToolMd(part);
|
|
2674
|
+
case "batch":
|
|
2675
|
+
return renderBatchToolMd(part);
|
|
2676
|
+
default:
|
|
2677
|
+
return renderGenericToolPartMd(part);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
function renderGenericToolPartMd(part) {
|
|
2681
|
+
const { tool, state } = part;
|
|
2682
|
+
const title = state.title || tool;
|
|
2683
|
+
const lines = [];
|
|
2684
|
+
lines.push(`**${tool}:** ${title}`);
|
|
2685
|
+
lines.push("");
|
|
2686
|
+
if (state.input) {
|
|
2687
|
+
lines.push("<details>");
|
|
2688
|
+
lines.push("<summary>Input</summary>");
|
|
2689
|
+
lines.push("");
|
|
2690
|
+
lines.push("```json");
|
|
2691
|
+
lines.push(JSON.stringify(state.input, null, 2));
|
|
2692
|
+
lines.push("```");
|
|
2693
|
+
lines.push("");
|
|
2694
|
+
lines.push("</details>");
|
|
2695
|
+
lines.push("");
|
|
2696
|
+
}
|
|
2697
|
+
if (state.output) {
|
|
2698
|
+
const outputLines = state.output.split(`
|
|
2699
|
+
`).length;
|
|
2700
|
+
if (outputLines > 15) {
|
|
2701
|
+
lines.push("<details>");
|
|
2702
|
+
lines.push("<summary>Output (click to expand)</summary>");
|
|
2703
|
+
lines.push("");
|
|
2704
|
+
lines.push("```");
|
|
2705
|
+
lines.push(state.output);
|
|
2706
|
+
lines.push("```");
|
|
2707
|
+
lines.push("");
|
|
2708
|
+
lines.push("</details>");
|
|
2709
|
+
} else {
|
|
2710
|
+
lines.push("```");
|
|
2711
|
+
lines.push(state.output);
|
|
2712
|
+
lines.push("```");
|
|
2713
|
+
}
|
|
2714
|
+
lines.push("");
|
|
2715
|
+
}
|
|
2716
|
+
if (state.error) {
|
|
2717
|
+
lines.push(`> **Error:** ${state.error}`);
|
|
2718
|
+
lines.push("");
|
|
2719
|
+
}
|
|
2720
|
+
return lines.join(`
|
|
2721
|
+
`);
|
|
2722
|
+
}
|
|
2723
|
+
function renderFilePartMd(part) {
|
|
2724
|
+
const filename = part.filename || "file";
|
|
2725
|
+
const isImage = part.mime.startsWith("image/");
|
|
2726
|
+
const isDataUrl = part.url.startsWith("data:");
|
|
2727
|
+
const lines = [];
|
|
2728
|
+
let sizeText = "";
|
|
2729
|
+
if (isDataUrl) {
|
|
2730
|
+
const base64Part = part.url.split(",")[1];
|
|
2731
|
+
if (base64Part) {
|
|
2732
|
+
const estimatedBytes = Math.floor(base64Part.length * 0.75);
|
|
2733
|
+
sizeText = ` (${formatBytes(estimatedBytes)})`;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
lines.push(`**File:** \`${filename}\` - ${part.mime}${sizeText}`);
|
|
2737
|
+
lines.push("");
|
|
2738
|
+
if (isImage && isDataUrl) {
|
|
2739
|
+
lines.push(``);
|
|
2740
|
+
lines.push("");
|
|
2741
|
+
}
|
|
2742
|
+
return lines.join(`
|
|
2743
|
+
`);
|
|
2744
|
+
}
|
|
2745
|
+
function renderSnapshotPartMd(part) {
|
|
2746
|
+
return `*Snapshot created: \`${part.snapshot}\`*`;
|
|
2747
|
+
}
|
|
2748
|
+
function renderPatchPartMd(part) {
|
|
2749
|
+
const fileCount = part.files.length;
|
|
2750
|
+
const lines = [];
|
|
2751
|
+
lines.push(`**File changes:** ${fileCount} file${fileCount !== 1 ? "s" : ""} (\`${part.hash.slice(0, 8)}\`)`);
|
|
2752
|
+
lines.push("");
|
|
2753
|
+
if (part.files.length > 0) {
|
|
2754
|
+
for (const file of part.files) {
|
|
2755
|
+
lines.push(`- \`${file}\``);
|
|
2756
|
+
}
|
|
2757
|
+
lines.push("");
|
|
2758
|
+
}
|
|
2759
|
+
return lines.join(`
|
|
2760
|
+
`);
|
|
2761
|
+
}
|
|
2762
|
+
function renderAgentPartMd(part) {
|
|
2763
|
+
const sourceText = part.source?.value ? `: ${part.source.value.slice(0, 100)}${part.source.value.length > 100 ? "..." : ""}` : "";
|
|
2764
|
+
return `**Agent:** \`${part.name}\`${sourceText}`;
|
|
2765
|
+
}
|
|
2766
|
+
function renderCompactionPartMd(part) {
|
|
2767
|
+
const typeLabel = part.auto ? "Auto-compacted" : "Manual compaction";
|
|
2768
|
+
return `*${typeLabel}*`;
|
|
2769
|
+
}
|
|
2770
|
+
function renderSubtaskPartMd(part) {
|
|
2771
|
+
const lines = [];
|
|
2772
|
+
lines.push(`**Subtask:** ${part.description} (${part.agent})`);
|
|
2773
|
+
lines.push("");
|
|
2774
|
+
if (part.command) {
|
|
2775
|
+
lines.push(`Command: \`${part.command}\``);
|
|
2776
|
+
lines.push("");
|
|
2777
|
+
}
|
|
2778
|
+
const promptLines = part.prompt.split(`
|
|
2779
|
+
`).length;
|
|
2780
|
+
if (promptLines > 5) {
|
|
2781
|
+
lines.push("<details>");
|
|
2782
|
+
lines.push("<summary>Prompt</summary>");
|
|
2783
|
+
lines.push("");
|
|
2784
|
+
lines.push(part.prompt);
|
|
2785
|
+
lines.push("");
|
|
2786
|
+
lines.push("</details>");
|
|
2787
|
+
} else {
|
|
2788
|
+
lines.push("> " + part.prompt.split(`
|
|
2789
|
+
`).join(`
|
|
2790
|
+
> `));
|
|
2791
|
+
}
|
|
2792
|
+
lines.push("");
|
|
2793
|
+
return lines.join(`
|
|
2794
|
+
`);
|
|
2795
|
+
}
|
|
2796
|
+
function renderRetryPartMd(part) {
|
|
2797
|
+
const codeText = part.error.code ? ` (code: ${part.error.code})` : "";
|
|
2798
|
+
return `**Retry #${part.attempt}:** ${part.error.name} - ${part.error.message}${codeText}`;
|
|
2799
|
+
}
|
|
2800
|
+
function renderGenericPartMd(part) {
|
|
2801
|
+
return `*[${part.type}]*`;
|
|
2802
|
+
}
|
|
2803
|
+
function renderPartMd(part) {
|
|
2804
|
+
switch (part.type) {
|
|
2805
|
+
case "text":
|
|
2806
|
+
return renderTextPartMd(part);
|
|
2807
|
+
case "reasoning":
|
|
2808
|
+
return renderReasoningPartMd(part);
|
|
2809
|
+
case "tool":
|
|
2810
|
+
return renderToolPartMd(part);
|
|
2811
|
+
case "file":
|
|
2812
|
+
return renderFilePartMd(part);
|
|
2813
|
+
case "snapshot":
|
|
2814
|
+
return renderSnapshotPartMd(part);
|
|
2815
|
+
case "patch":
|
|
2816
|
+
return renderPatchPartMd(part);
|
|
2817
|
+
case "agent":
|
|
2818
|
+
return renderAgentPartMd(part);
|
|
2819
|
+
case "compaction":
|
|
2820
|
+
return renderCompactionPartMd(part);
|
|
2821
|
+
case "subtask":
|
|
2822
|
+
return renderSubtaskPartMd(part);
|
|
2823
|
+
case "retry":
|
|
2824
|
+
return renderRetryPartMd(part);
|
|
2825
|
+
default:
|
|
2826
|
+
return renderGenericPartMd(part);
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// src/render/markdown/message.ts
|
|
2831
|
+
function renderUserMessageMd(message, parts) {
|
|
2832
|
+
const time = formatTime(message.time.created);
|
|
2833
|
+
const model = message.model ? `${message.model.providerID}/${message.model.modelID}` : "";
|
|
2834
|
+
const lines = [];
|
|
2835
|
+
lines.push("### User");
|
|
2836
|
+
lines.push("");
|
|
2837
|
+
lines.push(`*${time}*${model ? ` - ${model}` : ""}`);
|
|
2838
|
+
lines.push("");
|
|
2839
|
+
for (const part of parts) {
|
|
2840
|
+
const partMd = renderPartMd(part);
|
|
2841
|
+
if (partMd) {
|
|
2842
|
+
lines.push(partMd);
|
|
2843
|
+
lines.push("");
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
return lines.join(`
|
|
2847
|
+
`);
|
|
2848
|
+
}
|
|
2849
|
+
function renderAssistantMessageMd(message, parts) {
|
|
2850
|
+
const time = formatTime(message.time.created);
|
|
2851
|
+
const model = message.modelID || "";
|
|
2852
|
+
const lines = [];
|
|
2853
|
+
lines.push("### Assistant");
|
|
2854
|
+
lines.push("");
|
|
2855
|
+
lines.push(`*${time}*${model ? ` - ${model}` : ""}`);
|
|
2856
|
+
lines.push("");
|
|
2857
|
+
for (const part of parts) {
|
|
2858
|
+
const partMd = renderPartMd(part);
|
|
2859
|
+
if (partMd) {
|
|
2860
|
+
lines.push(partMd);
|
|
2861
|
+
lines.push("");
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
const stats = formatStatsMd(message);
|
|
2865
|
+
if (stats) {
|
|
2866
|
+
lines.push(`> ${stats}`);
|
|
2867
|
+
lines.push("");
|
|
2868
|
+
}
|
|
2869
|
+
return lines.join(`
|
|
2870
|
+
`);
|
|
2871
|
+
}
|
|
2872
|
+
function formatStatsMd(message) {
|
|
2873
|
+
const parts = [];
|
|
2874
|
+
if (message.tokens) {
|
|
2875
|
+
parts.push(`Tokens: ${formatTokens(message.tokens.input)} in / ${formatTokens(message.tokens.output)} out`);
|
|
2876
|
+
if (message.tokens.cache?.read) {
|
|
2877
|
+
parts.push(`Cache: ${formatTokens(message.tokens.cache.read)} read`);
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
if (message.cost !== undefined && message.cost > 0) {
|
|
2881
|
+
parts.push(`Cost: ${formatCost(message.cost)}`);
|
|
2882
|
+
}
|
|
2883
|
+
if (message.finish) {
|
|
2884
|
+
parts.push(`Finish: ${message.finish}`);
|
|
2885
|
+
}
|
|
2886
|
+
return parts.length > 0 ? parts.join(" | ") : null;
|
|
2887
|
+
}
|
|
2888
|
+
function renderMessageMd(messageWithParts) {
|
|
2889
|
+
const { message, parts } = messageWithParts;
|
|
2890
|
+
if (message.role === "user") {
|
|
2891
|
+
return renderUserMessageMd(message, parts);
|
|
2892
|
+
} else {
|
|
2893
|
+
return renderAssistantMessageMd(message, parts);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
// src/render/markdown/index.ts
|
|
2898
|
+
function renderFullTranscript(data, _context) {
|
|
2899
|
+
const { session: session2, projectName, messages, stats } = data;
|
|
2900
|
+
const lines = [];
|
|
2901
|
+
lines.push(`# ${session2.title}`);
|
|
2902
|
+
lines.push("");
|
|
2903
|
+
lines.push(`**Session:** \`${session2.id}\``);
|
|
2904
|
+
if (projectName) {
|
|
2905
|
+
lines.push(`**Project:** ${projectName}`);
|
|
2906
|
+
}
|
|
2907
|
+
lines.push(`**Created:** ${formatDateTime(session2.time.created)}`);
|
|
2908
|
+
lines.push(`**Messages:** ${messages.length}`);
|
|
2909
|
+
if (stats.model) {
|
|
2910
|
+
lines.push(`**Model:** ${stats.model}`);
|
|
2911
|
+
}
|
|
2912
|
+
if (stats.totalTokensInput > 0 || stats.totalTokensOutput > 0) {
|
|
2913
|
+
lines.push(`**Tokens:** ${formatTokens(stats.totalTokensInput)} in / ${formatTokens(stats.totalTokensOutput)} out`);
|
|
2914
|
+
}
|
|
2915
|
+
if (stats.totalCost > 0) {
|
|
2916
|
+
lines.push(`**Cost:** ${formatCost(stats.totalCost)}`);
|
|
2917
|
+
}
|
|
2918
|
+
lines.push("");
|
|
2919
|
+
lines.push("---");
|
|
2920
|
+
lines.push("");
|
|
2921
|
+
for (const msg of messages) {
|
|
2922
|
+
lines.push(renderMessageMd(msg));
|
|
2923
|
+
lines.push("");
|
|
2924
|
+
lines.push("---");
|
|
2925
|
+
lines.push("");
|
|
2926
|
+
}
|
|
2927
|
+
return lines.join(`
|
|
2928
|
+
`);
|
|
2929
|
+
}
|
|
2930
|
+
function generateMarkdown(options) {
|
|
2931
|
+
const { session: session2, messages, projectName, stats, includeHeader = true } = options;
|
|
2932
|
+
const data = {
|
|
2933
|
+
session: session2,
|
|
2934
|
+
projectName,
|
|
2935
|
+
messages,
|
|
2936
|
+
timeline: [],
|
|
2937
|
+
stats
|
|
2938
|
+
};
|
|
2939
|
+
const context = {
|
|
2940
|
+
format: "markdown"
|
|
2941
|
+
};
|
|
2942
|
+
if (!includeHeader) {
|
|
2943
|
+
const lines = [];
|
|
2944
|
+
for (const msg of messages) {
|
|
2945
|
+
lines.push(renderMessageMd(msg));
|
|
2946
|
+
lines.push("");
|
|
2947
|
+
lines.push("---");
|
|
2948
|
+
lines.push("");
|
|
2949
|
+
}
|
|
2950
|
+
return lines.join(`
|
|
2951
|
+
`);
|
|
2952
|
+
}
|
|
2953
|
+
return renderFullTranscript(data, context);
|
|
2954
|
+
}
|
|
2955
|
+
// src/server.ts
|
|
2956
|
+
import { resolve, join as join3, sep } from "path";
|
|
2957
|
+
function isPathSafe(rootDir, targetPath) {
|
|
2958
|
+
const resolvedRoot = resolve(rootDir);
|
|
2959
|
+
const resolvedTarget = resolve(targetPath);
|
|
2960
|
+
return resolvedTarget.startsWith(resolvedRoot + sep) || resolvedTarget === resolvedRoot;
|
|
2961
|
+
}
|
|
2962
|
+
function createRequestHandler(rootDir) {
|
|
2963
|
+
const ROOT_DIR = resolve(rootDir);
|
|
2964
|
+
return async function handleRequest(req) {
|
|
2965
|
+
const url = new URL(req.url);
|
|
2966
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2967
|
+
return new Response("Method Not Allowed", {
|
|
2968
|
+
status: 405,
|
|
2969
|
+
headers: { Allow: "GET, HEAD" }
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
let pathname;
|
|
2973
|
+
try {
|
|
2974
|
+
pathname = decodeURIComponent(url.pathname);
|
|
2975
|
+
} catch {
|
|
2976
|
+
return new Response("Bad Request", { status: 400 });
|
|
2977
|
+
}
|
|
2978
|
+
pathname = pathname.replace(/\0/g, "");
|
|
2979
|
+
const targetPath = join3(ROOT_DIR, pathname);
|
|
2980
|
+
if (!isPathSafe(ROOT_DIR, targetPath)) {
|
|
2981
|
+
return new Response("Forbidden", { status: 403 });
|
|
2982
|
+
}
|
|
2983
|
+
let file = Bun.file(targetPath);
|
|
2984
|
+
let fileExists = await file.exists();
|
|
2985
|
+
if (pathname.endsWith("/") || !fileExists) {
|
|
2986
|
+
const indexPath = join3(targetPath, "index.html");
|
|
2987
|
+
const indexFile = Bun.file(indexPath);
|
|
2988
|
+
if (await indexFile.exists()) {
|
|
2989
|
+
file = indexFile;
|
|
2990
|
+
fileExists = true;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
if (!fileExists) {
|
|
2994
|
+
return new Response("Not Found", {
|
|
2995
|
+
status: 404,
|
|
2996
|
+
headers: { "Content-Type": "text/plain" }
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
const content = await file.arrayBuffer();
|
|
3000
|
+
const etag = `W/"${Bun.hash(new Uint8Array(content)).toString(16)}"`;
|
|
3001
|
+
const ifNoneMatch = req.headers.get("If-None-Match");
|
|
3002
|
+
if (ifNoneMatch === etag) {
|
|
3003
|
+
return new Response(null, {
|
|
3004
|
+
status: 304,
|
|
3005
|
+
headers: { ETag: etag }
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
const isHashed = /\.[a-f0-9]{8,}\.(js|css|png|jpg|jpeg|gif|svg|woff2?)$/i.test(targetPath);
|
|
3009
|
+
const responseHeaders = {
|
|
3010
|
+
"Content-Type": file.type,
|
|
3011
|
+
"Content-Length": String(content.byteLength),
|
|
3012
|
+
ETag: etag,
|
|
3013
|
+
"Cache-Control": isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600"
|
|
3014
|
+
};
|
|
3015
|
+
if (req.method === "HEAD") {
|
|
3016
|
+
return new Response(null, { headers: responseHeaders });
|
|
3017
|
+
}
|
|
3018
|
+
return new Response(content, { headers: responseHeaders });
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
async function serve(options) {
|
|
3022
|
+
const { directory, port, open = true } = options;
|
|
3023
|
+
const handleRequest = createRequestHandler(directory);
|
|
3024
|
+
let server;
|
|
3025
|
+
try {
|
|
3026
|
+
server = Bun.serve({
|
|
3027
|
+
port,
|
|
3028
|
+
fetch: handleRequest,
|
|
3029
|
+
error(error) {
|
|
3030
|
+
console.error("Server error:", error);
|
|
3031
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
} catch (err) {
|
|
3035
|
+
const error = err;
|
|
3036
|
+
if (error.code === "EADDRINUSE") {
|
|
3037
|
+
console.error(`Error: Port ${port} is already in use`);
|
|
3038
|
+
process.exit(1);
|
|
3039
|
+
}
|
|
3040
|
+
throw err;
|
|
3041
|
+
}
|
|
3042
|
+
const serverUrl = `http://localhost:${port}`;
|
|
3043
|
+
console.log(`
|
|
3044
|
+
Server running at ${serverUrl}`);
|
|
3045
|
+
console.log(`Press Ctrl+C to stop
|
|
3046
|
+
`);
|
|
3047
|
+
if (open) {
|
|
3048
|
+
openBrowser(serverUrl);
|
|
3049
|
+
}
|
|
3050
|
+
const sigintHandler = () => shutdown("SIGINT");
|
|
3051
|
+
const sigtermHandler = () => shutdown("SIGTERM");
|
|
3052
|
+
function shutdown(signal) {
|
|
3053
|
+
console.log(`
|
|
3054
|
+
Received ${signal}, shutting down...`);
|
|
3055
|
+
process.off("SIGINT", sigintHandler);
|
|
3056
|
+
process.off("SIGTERM", sigtermHandler);
|
|
3057
|
+
server.stop();
|
|
3058
|
+
console.log("Server stopped");
|
|
3059
|
+
process.exit(0);
|
|
3060
|
+
}
|
|
3061
|
+
process.on("SIGINT", sigintHandler);
|
|
3062
|
+
process.on("SIGTERM", sigtermHandler);
|
|
3063
|
+
await new Promise(() => {});
|
|
3064
|
+
}
|
|
3065
|
+
function openBrowser(url) {
|
|
3066
|
+
const platform = process.platform;
|
|
3067
|
+
if (platform === "darwin") {
|
|
3068
|
+
Bun.spawn(["open", url]);
|
|
3069
|
+
} else if (platform === "win32") {
|
|
3070
|
+
Bun.spawn(["cmd", "/c", "start", "", url]);
|
|
3071
|
+
} else {
|
|
3072
|
+
Bun.spawn(["xdg-open", url]);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// src/utils/update-notifier.ts
|
|
3077
|
+
import { homedir as homedir2 } from "os";
|
|
3078
|
+
import { join as join4 } from "path";
|
|
3079
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
3080
|
+
var PACKAGE_NAME = "opencode-replay";
|
|
3081
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
3082
|
+
function detectPackageManager() {
|
|
3083
|
+
const modulePath = import.meta.dir;
|
|
3084
|
+
if (modulePath.includes(".bun") || modulePath.includes("bun/install")) {
|
|
3085
|
+
return "bun";
|
|
3086
|
+
}
|
|
3087
|
+
if (process.env.BUN_INSTALL) {
|
|
3088
|
+
return "bun";
|
|
3089
|
+
}
|
|
3090
|
+
if (typeof Bun !== "undefined") {
|
|
3091
|
+
const bunInstallPath = join4(homedir2(), ".bun");
|
|
3092
|
+
try {
|
|
3093
|
+
if (Bun.file(bunInstallPath).size) {
|
|
3094
|
+
return "bun";
|
|
3095
|
+
}
|
|
3096
|
+
} catch {}
|
|
3097
|
+
}
|
|
3098
|
+
return "npm";
|
|
3099
|
+
}
|
|
3100
|
+
function getCachePath() {
|
|
3101
|
+
const cacheDir = process.env.XDG_CACHE_HOME || join4(homedir2(), ".cache");
|
|
2253
3102
|
return join4(cacheDir, "opencode-replay", "update-check.json");
|
|
2254
3103
|
}
|
|
2255
3104
|
async function readCache() {
|
|
@@ -2262,11 +3111,11 @@ async function readCache() {
|
|
|
2262
3111
|
} catch {}
|
|
2263
3112
|
return null;
|
|
2264
3113
|
}
|
|
2265
|
-
async function writeCache(
|
|
3114
|
+
async function writeCache(data2) {
|
|
2266
3115
|
try {
|
|
2267
3116
|
const cachePath = getCachePath();
|
|
2268
3117
|
await mkdir2(join4(cachePath, ".."), { recursive: true });
|
|
2269
|
-
await Bun.write(cachePath, JSON.stringify(
|
|
3118
|
+
await Bun.write(cachePath, JSON.stringify(data2));
|
|
2270
3119
|
} catch {}
|
|
2271
3120
|
}
|
|
2272
3121
|
async function fetchLatestVersion() {
|
|
@@ -2280,8 +3129,8 @@ async function fetchLatestVersion() {
|
|
|
2280
3129
|
if (!response.ok) {
|
|
2281
3130
|
return null;
|
|
2282
3131
|
}
|
|
2283
|
-
const
|
|
2284
|
-
return
|
|
3132
|
+
const data2 = await response.json();
|
|
3133
|
+
return data2.version || null;
|
|
2285
3134
|
} catch {
|
|
2286
3135
|
return null;
|
|
2287
3136
|
}
|
|
@@ -2379,6 +3228,74 @@ async function notifyIfUpdateAvailable(currentVersion) {
|
|
|
2379
3228
|
}
|
|
2380
3229
|
}
|
|
2381
3230
|
|
|
3231
|
+
// src/gist.ts
|
|
3232
|
+
var {spawn } = globalThis.Bun;
|
|
3233
|
+
|
|
3234
|
+
class GistError extends Error {
|
|
3235
|
+
code;
|
|
3236
|
+
constructor(message2, code) {
|
|
3237
|
+
super(message2);
|
|
3238
|
+
this.code = code;
|
|
3239
|
+
this.name = "GistError";
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
async function isGhInstalled() {
|
|
3243
|
+
try {
|
|
3244
|
+
const proc = spawn(["gh", "--version"], {
|
|
3245
|
+
stdout: "pipe",
|
|
3246
|
+
stderr: "pipe"
|
|
3247
|
+
});
|
|
3248
|
+
const exitCode = await proc.exited;
|
|
3249
|
+
return exitCode === 0;
|
|
3250
|
+
} catch {
|
|
3251
|
+
return false;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
async function isGhAuthenticated() {
|
|
3255
|
+
try {
|
|
3256
|
+
const proc = spawn(["gh", "auth", "status"], {
|
|
3257
|
+
stdout: "pipe",
|
|
3258
|
+
stderr: "pipe"
|
|
3259
|
+
});
|
|
3260
|
+
const exitCode = await proc.exited;
|
|
3261
|
+
return exitCode === 0;
|
|
3262
|
+
} catch {
|
|
3263
|
+
return false;
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
async function createGist(files, options = {}) {
|
|
3267
|
+
if (!await isGhInstalled()) {
|
|
3268
|
+
throw new GistError("GitHub CLI (gh) not found. Install it from https://cli.github.com/", "NOT_INSTALLED");
|
|
3269
|
+
}
|
|
3270
|
+
if (!await isGhAuthenticated()) {
|
|
3271
|
+
throw new GistError("Not authenticated with GitHub CLI. Run: gh auth login", "NOT_AUTHENTICATED");
|
|
3272
|
+
}
|
|
3273
|
+
const cmd = ["gh", "gist", "create"];
|
|
3274
|
+
if (options.description) {
|
|
3275
|
+
cmd.push("--desc", options.description);
|
|
3276
|
+
}
|
|
3277
|
+
if (options.public) {
|
|
3278
|
+
cmd.push("--public");
|
|
3279
|
+
}
|
|
3280
|
+
cmd.push(...files);
|
|
3281
|
+
const proc = spawn(cmd, { stdout: "pipe", stderr: "pipe" });
|
|
3282
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
3283
|
+
new Response(proc.stdout).text(),
|
|
3284
|
+
new Response(proc.stderr).text(),
|
|
3285
|
+
proc.exited
|
|
3286
|
+
]);
|
|
3287
|
+
if (exitCode !== 0) {
|
|
3288
|
+
throw new GistError(`gh gist create failed: ${stderr.trim() || "Unknown error"}`, "CREATE_FAILED");
|
|
3289
|
+
}
|
|
3290
|
+
const gistUrl = stdout.trim();
|
|
3291
|
+
const gistId = gistUrl.split("/").pop() || "";
|
|
3292
|
+
if (!gistId) {
|
|
3293
|
+
throw new GistError(`Failed to parse gist ID from output: ${gistUrl}`, "CREATE_FAILED");
|
|
3294
|
+
}
|
|
3295
|
+
const previewUrl = `https://gisthost.github.io/?${gistId}/index.html`;
|
|
3296
|
+
return { gistId, gistUrl, previewUrl };
|
|
3297
|
+
}
|
|
3298
|
+
|
|
2382
3299
|
// src/index.ts
|
|
2383
3300
|
var colors = {
|
|
2384
3301
|
reset: "\x1B[0m",
|
|
@@ -2401,19 +3318,19 @@ function color(text, ...codes) {
|
|
|
2401
3318
|
}
|
|
2402
3319
|
var quietMode = false;
|
|
2403
3320
|
var verboseMode = false;
|
|
2404
|
-
function log(
|
|
3321
|
+
function log(message2) {
|
|
2405
3322
|
if (!quietMode) {
|
|
2406
|
-
console.log(
|
|
3323
|
+
console.log(message2);
|
|
2407
3324
|
}
|
|
2408
3325
|
}
|
|
2409
|
-
function debug(
|
|
3326
|
+
function debug(message2) {
|
|
2410
3327
|
if (verboseMode && !quietMode) {
|
|
2411
|
-
console.log(color("[debug]", colors.gray) + " " +
|
|
3328
|
+
console.log(color("[debug]", colors.gray) + " " + message2);
|
|
2412
3329
|
}
|
|
2413
3330
|
}
|
|
2414
|
-
function writeProgress(
|
|
3331
|
+
function writeProgress(message2) {
|
|
2415
3332
|
if (!quietMode) {
|
|
2416
|
-
process.stdout.write(
|
|
3333
|
+
process.stdout.write(message2);
|
|
2417
3334
|
}
|
|
2418
3335
|
}
|
|
2419
3336
|
function formatProgress(progress) {
|
|
@@ -2532,6 +3449,27 @@ var { values } = parseArgs({
|
|
|
2532
3449
|
type: "string",
|
|
2533
3450
|
description: "GitHub repo (OWNER/NAME) for commit links"
|
|
2534
3451
|
},
|
|
3452
|
+
format: {
|
|
3453
|
+
type: "string",
|
|
3454
|
+
short: "f",
|
|
3455
|
+
default: "html",
|
|
3456
|
+
description: "Output format: html (default), md"
|
|
3457
|
+
},
|
|
3458
|
+
stdout: {
|
|
3459
|
+
type: "boolean",
|
|
3460
|
+
default: false,
|
|
3461
|
+
description: "Output to stdout (markdown only)"
|
|
3462
|
+
},
|
|
3463
|
+
gist: {
|
|
3464
|
+
type: "boolean",
|
|
3465
|
+
default: false,
|
|
3466
|
+
description: "Upload to GitHub Gist after generation"
|
|
3467
|
+
},
|
|
3468
|
+
"gist-public": {
|
|
3469
|
+
type: "boolean",
|
|
3470
|
+
default: false,
|
|
3471
|
+
description: "Make gist public (default: secret)"
|
|
3472
|
+
},
|
|
2535
3473
|
help: {
|
|
2536
3474
|
type: "boolean",
|
|
2537
3475
|
short: "h",
|
|
@@ -2557,8 +3495,12 @@ Usage:
|
|
|
2557
3495
|
Options:
|
|
2558
3496
|
--all Generate for all projects (default: current project only)
|
|
2559
3497
|
-a, --auto Auto-name output directory from project/session name
|
|
2560
|
-
-o, --output <
|
|
3498
|
+
-o, --output <path> Output directory (HTML) or file (markdown)
|
|
2561
3499
|
-s, --session <id> Generate for specific session only
|
|
3500
|
+
-f, --format <type> Output format: html (default), md
|
|
3501
|
+
--stdout Output to stdout (markdown only, requires --session)
|
|
3502
|
+
--gist Upload HTML to GitHub Gist after generation
|
|
3503
|
+
--gist-public Make gist public (default: secret)
|
|
2562
3504
|
--json Include raw JSON export alongside HTML
|
|
2563
3505
|
--open Open in browser after generation
|
|
2564
3506
|
--storage <path> Custom storage path (default: ~/.local/share/opencode/storage)
|
|
@@ -2572,7 +3514,7 @@ Options:
|
|
|
2572
3514
|
-v, --version Show version
|
|
2573
3515
|
|
|
2574
3516
|
Examples:
|
|
2575
|
-
opencode-replay # Current project's sessions
|
|
3517
|
+
opencode-replay # Current project's sessions (HTML)
|
|
2576
3518
|
opencode-replay --all # All projects
|
|
2577
3519
|
opencode-replay -a # Auto-name output (e.g., ./my-project-replay)
|
|
2578
3520
|
opencode-replay -o ./my-transcripts # Custom output directory
|
|
@@ -2580,7 +3522,19 @@ Examples:
|
|
|
2580
3522
|
opencode-replay --serve # Generate and serve via HTTP
|
|
2581
3523
|
opencode-replay --serve --port 8080 # Serve on custom port
|
|
2582
3524
|
opencode-replay --serve --no-generate -o ./existing # Serve existing output
|
|
2583
|
-
opencode-replay --repo sst/opencode
|
|
3525
|
+
opencode-replay --repo sst/opencode # Add GitHub links to git commits
|
|
3526
|
+
|
|
3527
|
+
Markdown Output:
|
|
3528
|
+
opencode-replay -f md -s ses_xxx # Markdown to stdout
|
|
3529
|
+
opencode-replay -f md -s ses_xxx -o transcript # Markdown to file
|
|
3530
|
+
opencode-replay -f md -s ses_xxx | gh gist create --filename transcript.md -
|
|
3531
|
+
|
|
3532
|
+
GitHub Gist:
|
|
3533
|
+
opencode-replay --gist # Upload to secret gist
|
|
3534
|
+
opencode-replay --gist --gist-public # Upload to public gist
|
|
3535
|
+
opencode-replay -s ses_xxx --gist # Upload specific session to gist
|
|
3536
|
+
|
|
3537
|
+
Requires GitHub CLI (https://cli.github.com/) to be installed and authenticated.
|
|
2584
3538
|
`);
|
|
2585
3539
|
process.exit(0);
|
|
2586
3540
|
}
|
|
@@ -2601,6 +3555,36 @@ if (isNaN(port) || port < 1 || port > 65535) {
|
|
|
2601
3555
|
console.error("Error: Invalid port number");
|
|
2602
3556
|
process.exit(1);
|
|
2603
3557
|
}
|
|
3558
|
+
var format = (values.format ?? "html").toLowerCase();
|
|
3559
|
+
if (format !== "html" && format !== "md" && format !== "markdown") {
|
|
3560
|
+
console.error(color("Error:", colors.red, colors.bold) + ` Invalid format: ${values.format}`);
|
|
3561
|
+
console.error("Valid formats: html, md (or markdown)");
|
|
3562
|
+
process.exit(1);
|
|
3563
|
+
}
|
|
3564
|
+
var isMarkdownFormat = format === "md" || format === "markdown";
|
|
3565
|
+
if (values.stdout) {
|
|
3566
|
+
if (!isMarkdownFormat) {
|
|
3567
|
+
console.error(color("Error:", colors.red, colors.bold) + " --stdout requires --format md");
|
|
3568
|
+
console.error("HTML output to stdout is not supported.");
|
|
3569
|
+
process.exit(1);
|
|
3570
|
+
}
|
|
3571
|
+
if (!values.session) {
|
|
3572
|
+
console.error(color("Error:", colors.red, colors.bold) + " --stdout requires --session");
|
|
3573
|
+
console.error("Specify a session with -s <session_id> for stdout output.");
|
|
3574
|
+
process.exit(1);
|
|
3575
|
+
}
|
|
3576
|
+
quietMode = true;
|
|
3577
|
+
}
|
|
3578
|
+
if (values.gist) {
|
|
3579
|
+
if (isMarkdownFormat) {
|
|
3580
|
+
console.error(color("Error:", colors.red, colors.bold) + " --gist requires --format html (default)");
|
|
3581
|
+
console.error("Use --format md | gh gist create --filename transcript.md - for markdown gists.");
|
|
3582
|
+
process.exit(1);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
if (values["gist-public"] && !values.gist) {
|
|
3586
|
+
console.error(color("Warning:", colors.yellow, colors.bold) + " --gist-public has no effect without --gist");
|
|
3587
|
+
}
|
|
2604
3588
|
try {
|
|
2605
3589
|
await readdir2(storagePath);
|
|
2606
3590
|
const projectDir = join5(storagePath, "project");
|
|
@@ -2651,6 +3635,61 @@ if (values.repo && !repoInfo) {
|
|
|
2651
3635
|
console.error("Expected format: OWNER/NAME (e.g., sst/opencode)");
|
|
2652
3636
|
process.exit(1);
|
|
2653
3637
|
}
|
|
3638
|
+
if (isMarkdownFormat) {
|
|
3639
|
+
if (values.all) {
|
|
3640
|
+
console.error(color("Error:", colors.red, colors.bold) + " --format md does not support --all");
|
|
3641
|
+
console.error("Markdown output is single-session only. Use -s <session_id>");
|
|
3642
|
+
process.exit(1);
|
|
3643
|
+
}
|
|
3644
|
+
if (values.serve) {
|
|
3645
|
+
console.error(color("Error:", colors.red, colors.bold) + " --serve is not supported with --format md");
|
|
3646
|
+
console.error("Use --format html (default) for serving via HTTP.");
|
|
3647
|
+
process.exit(1);
|
|
3648
|
+
}
|
|
3649
|
+
if (!values.session) {
|
|
3650
|
+
console.error(color("Error:", colors.red, colors.bold) + " --format md requires --session");
|
|
3651
|
+
console.error("Specify a session with -s <session_id>");
|
|
3652
|
+
process.exit(1);
|
|
3653
|
+
}
|
|
3654
|
+
try {
|
|
3655
|
+
const projects = await listProjects(storagePath);
|
|
3656
|
+
let foundSession = null;
|
|
3657
|
+
for (const project of projects) {
|
|
3658
|
+
const sessions = await listSessions(storagePath, project.id);
|
|
3659
|
+
const session2 = sessions.find((s) => s.id === values.session);
|
|
3660
|
+
if (session2) {
|
|
3661
|
+
foundSession = { session: session2, projectName: project.name };
|
|
3662
|
+
break;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
if (!foundSession) {
|
|
3666
|
+
console.error(color("Error:", colors.red, colors.bold) + ` Session not found: ${values.session}`);
|
|
3667
|
+
process.exit(1);
|
|
3668
|
+
}
|
|
3669
|
+
const messages = await getMessagesWithParts(storagePath, foundSession.session.id);
|
|
3670
|
+
const stats = calculateSessionStats(messages);
|
|
3671
|
+
const markdown2 = generateMarkdown({
|
|
3672
|
+
session: foundSession.session,
|
|
3673
|
+
messages,
|
|
3674
|
+
projectName: foundSession.projectName,
|
|
3675
|
+
stats,
|
|
3676
|
+
includeHeader: true
|
|
3677
|
+
});
|
|
3678
|
+
if (values.stdout) {
|
|
3679
|
+
process.stdout.write(markdown2);
|
|
3680
|
+
} else if (values.output) {
|
|
3681
|
+
const outputPath = values.output.endsWith(".md") ? values.output : `${values.output}.md`;
|
|
3682
|
+
await Bun.write(outputPath, markdown2);
|
|
3683
|
+
console.log(resolve2(outputPath));
|
|
3684
|
+
} else {
|
|
3685
|
+
process.stdout.write(markdown2);
|
|
3686
|
+
}
|
|
3687
|
+
} catch (error) {
|
|
3688
|
+
console.error(color("Error:", colors.red, colors.bold) + " " + (error instanceof Error ? error.message : error));
|
|
3689
|
+
process.exit(1);
|
|
3690
|
+
}
|
|
3691
|
+
process.exit(0);
|
|
3692
|
+
}
|
|
2654
3693
|
log(color("opencode-replay", colors.bold, colors.cyan));
|
|
2655
3694
|
log(color("---------------", colors.dim));
|
|
2656
3695
|
log(color("Storage:", colors.dim) + ` ${storagePath}`);
|
|
@@ -2682,6 +3721,7 @@ if (values["no-generate"]) {
|
|
|
2682
3721
|
sessionId: values.session,
|
|
2683
3722
|
includeJson: values.json ?? false,
|
|
2684
3723
|
repo: repoInfo,
|
|
3724
|
+
gistMode: values.gist ?? false,
|
|
2685
3725
|
onProgress: (progress) => {
|
|
2686
3726
|
const msg = formatProgress(progress);
|
|
2687
3727
|
if (msg) {
|
|
@@ -2696,13 +3736,52 @@ if (values["no-generate"]) {
|
|
|
2696
3736
|
process.exit(1);
|
|
2697
3737
|
}
|
|
2698
3738
|
log(color("Done!", colors.green, colors.bold) + ` Generated ${formatStats(stats)}`);
|
|
2699
|
-
|
|
3739
|
+
if (!values.gist) {
|
|
3740
|
+
console.log(resolve2(outputDir));
|
|
3741
|
+
}
|
|
2700
3742
|
} catch (error) {
|
|
2701
3743
|
process.stdout.write("\r\x1B[K");
|
|
2702
3744
|
console.error(color("Error:", colors.red, colors.bold) + " " + (error instanceof Error ? error.message : error));
|
|
2703
3745
|
process.exit(1);
|
|
2704
3746
|
}
|
|
2705
3747
|
}
|
|
3748
|
+
if (values.gist && !values["no-generate"]) {
|
|
3749
|
+
log("");
|
|
3750
|
+
log(color("Uploading to GitHub Gist...", colors.cyan));
|
|
3751
|
+
try {
|
|
3752
|
+
const gistFiles = await collectGistFiles(resolve2(outputDir));
|
|
3753
|
+
if (gistFiles.length === 0) {
|
|
3754
|
+
console.error(color("Error:", colors.red, colors.bold) + " No files found to upload");
|
|
3755
|
+
process.exit(1);
|
|
3756
|
+
}
|
|
3757
|
+
debug(`Found ${gistFiles.length} files to upload`);
|
|
3758
|
+
const description = values.session ? `OpenCode transcript: ${values.session}` : values.all ? "OpenCode transcripts: All projects" : "OpenCode transcripts";
|
|
3759
|
+
const result = await createGist(gistFiles, {
|
|
3760
|
+
public: values["gist-public"] ?? false,
|
|
3761
|
+
description
|
|
3762
|
+
});
|
|
3763
|
+
log("");
|
|
3764
|
+
log(color("Gist created!", colors.green, colors.bold));
|
|
3765
|
+
log(color("Gist URL:", colors.dim) + ` ${result.gistUrl}`);
|
|
3766
|
+
log(color("Preview:", colors.dim) + ` ${result.previewUrl}`);
|
|
3767
|
+
console.log(result.previewUrl);
|
|
3768
|
+
if (values.open) {
|
|
3769
|
+
openInBrowser(result.previewUrl);
|
|
3770
|
+
}
|
|
3771
|
+
} catch (error) {
|
|
3772
|
+
if (error instanceof GistError) {
|
|
3773
|
+
console.error(color("Error:", colors.red, colors.bold) + ` ${error.message}`);
|
|
3774
|
+
if (error.code === "NOT_INSTALLED") {
|
|
3775
|
+
console.error(color("Install GitHub CLI:", colors.dim) + " https://cli.github.com/");
|
|
3776
|
+
} else if (error.code === "NOT_AUTHENTICATED") {
|
|
3777
|
+
console.error(color("Authenticate with:", colors.dim) + " gh auth login");
|
|
3778
|
+
}
|
|
3779
|
+
} else {
|
|
3780
|
+
console.error(color("Error:", colors.red, colors.bold) + " " + (error instanceof Error ? error.message : error));
|
|
3781
|
+
}
|
|
3782
|
+
process.exit(1);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
2706
3785
|
if (values.serve) {
|
|
2707
3786
|
await notifyIfUpdateAvailable(currentVersion);
|
|
2708
3787
|
await serve({
|
|
@@ -2710,7 +3789,7 @@ if (values.serve) {
|
|
|
2710
3789
|
port,
|
|
2711
3790
|
open: values.open ?? true
|
|
2712
3791
|
});
|
|
2713
|
-
} else if (values.open) {
|
|
3792
|
+
} else if (values.open && !values.gist) {
|
|
2714
3793
|
const indexPath = resolve2(outputDir, "index.html");
|
|
2715
3794
|
openInBrowser(indexPath);
|
|
2716
3795
|
}
|
|
@@ -2727,3 +3806,16 @@ function openInBrowser(target) {
|
|
|
2727
3806
|
Bun.spawn(["xdg-open", target]);
|
|
2728
3807
|
}
|
|
2729
3808
|
}
|
|
3809
|
+
async function collectGistFiles(dir) {
|
|
3810
|
+
const files = [];
|
|
3811
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
3812
|
+
for (const entry of entries) {
|
|
3813
|
+
const fullPath = join5(dir, entry.name);
|
|
3814
|
+
if (entry.isDirectory()) {
|
|
3815
|
+
files.push(...await collectGistFiles(fullPath));
|
|
3816
|
+
} else if (entry.name.endsWith(".html") || entry.name.endsWith(".css") || entry.name.endsWith(".js")) {
|
|
3817
|
+
files.push(fullPath);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
return files;
|
|
3821
|
+
}
|