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/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/html.ts
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
- async function generateSessionHtml(storagePath, outputDir, session2, projectName, repo, includeJson) {
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
- // src/server.ts
2107
- import { resolve, join as join3, sep } from "path";
2108
- function isPathSafe(rootDir, targetPath) {
2109
- const resolvedRoot = resolve(rootDir);
2110
- const resolvedTarget = resolve(targetPath);
2111
- return resolvedTarget.startsWith(resolvedRoot + sep) || resolvedTarget === resolvedRoot;
2112
- }
2113
- function createRequestHandler(rootDir) {
2114
- const ROOT_DIR = resolve(rootDir);
2115
- return async function handleRequest(req) {
2116
- const url = new URL(req.url);
2117
- if (req.method !== "GET" && req.method !== "HEAD") {
2118
- return new Response("Method Not Allowed", {
2119
- status: 405,
2120
- headers: { Allow: "GET, HEAD" }
2121
- });
2122
- }
2123
- let pathname;
2124
- try {
2125
- pathname = decodeURIComponent(url.pathname);
2126
- } catch {
2127
- return new Response("Bad Request", { status: 400 });
2128
- }
2129
- pathname = pathname.replace(/\0/g, "");
2130
- const targetPath = join3(ROOT_DIR, pathname);
2131
- if (!isPathSafe(ROOT_DIR, targetPath)) {
2132
- return new Response("Forbidden", { status: 403 });
2133
- }
2134
- let file = Bun.file(targetPath);
2135
- let fileExists = await file.exists();
2136
- if (pathname.endsWith("/") || !fileExists) {
2137
- const indexPath = join3(targetPath, "index.html");
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
- throw err;
2161
+ lines.push("");
2192
2162
  }
2193
- const serverUrl = `http://localhost:${port}`;
2194
- console.log(`
2195
- Server running at ${serverUrl}`);
2196
- console.log(`Press Ctrl+C to stop
2163
+ if (error) {
2164
+ lines.push(`> **Error:** ${error}`);
2165
+ lines.push("");
2166
+ }
2167
+ return lines.join(`
2197
2168
  `);
2198
- if (open) {
2199
- openBrowser(serverUrl);
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
- const sigintHandler = () => shutdown("SIGINT");
2202
- const sigtermHandler = () => shutdown("SIGTERM");
2203
- function shutdown(signal) {
2204
- console.log(`
2205
- Received ${signal}, shutting down...`);
2206
- process.off("SIGINT", sigintHandler);
2207
- process.off("SIGTERM", sigtermHandler);
2208
- server.stop();
2209
- console.log("Server stopped");
2210
- process.exit(0);
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
- process.on("SIGINT", sigintHandler);
2213
- process.on("SIGTERM", sigtermHandler);
2214
- await new Promise(() => {});
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/utils/update-notifier.ts
2228
- import { homedir as homedir2 } from "os";
2229
- import { join as join4 } from "path";
2230
- import { mkdir as mkdir2 } from "fs/promises";
2231
- var PACKAGE_NAME = "opencode-replay";
2232
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
2233
- function detectPackageManager() {
2234
- const modulePath = import.meta.dir;
2235
- if (modulePath.includes(".bun") || modulePath.includes("bun/install")) {
2236
- return "bun";
2237
- }
2238
- if (process.env.BUN_INSTALL) {
2239
- return "bun";
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 (typeof Bun !== "undefined") {
2242
- const bunInstallPath = join4(homedir2(), ".bun");
2243
- try {
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 "npm";
2255
+ return lines.join(`
2256
+ `);
2250
2257
  }
2251
- function getCachePath() {
2252
- const cacheDir = process.env.XDG_CACHE_HOME || join4(homedir2(), ".cache");
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(`![${filename}](${part.url})`);
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(data) {
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(data));
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 data = await response.json();
2284
- return data.version || null;
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(message) {
3321
+ function log(message2) {
2405
3322
  if (!quietMode) {
2406
- console.log(message);
3323
+ console.log(message2);
2407
3324
  }
2408
3325
  }
2409
- function debug(message) {
3326
+ function debug(message2) {
2410
3327
  if (verboseMode && !quietMode) {
2411
- console.log(color("[debug]", colors.gray) + " " + message);
3328
+ console.log(color("[debug]", colors.gray) + " " + message2);
2412
3329
  }
2413
3330
  }
2414
- function writeProgress(message) {
3331
+ function writeProgress(message2) {
2415
3332
  if (!quietMode) {
2416
- process.stdout.write(message);
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 <dir> Output directory (default: ./opencode-replay-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 # Add GitHub links to git commits
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
- console.log(resolve2(outputDir));
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
+ }