@oh-my-pi/pi-coding-agent 16.0.6 → 16.0.8

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli.js +4760 -12462
  3. package/dist/types/cli/update-cli.d.ts +11 -0
  4. package/dist/types/debug/remote-debugger.d.ts +45 -0
  5. package/dist/types/internal-urls/docs-index.d.ts +19 -0
  6. package/dist/types/markit/converters/docx.d.ts +6 -0
  7. package/dist/types/markit/converters/epub.d.ts +15 -0
  8. package/dist/types/markit/converters/pdf/columns.d.ts +35 -0
  9. package/dist/types/markit/converters/pdf/extract.d.ts +10 -0
  10. package/dist/types/markit/converters/pdf/grid.d.ts +25 -0
  11. package/dist/types/markit/converters/pdf/headers.d.ts +24 -0
  12. package/dist/types/markit/converters/pdf/index.d.ts +6 -0
  13. package/dist/types/markit/converters/pdf/render.d.ts +24 -0
  14. package/dist/types/markit/converters/pdf/types.d.ts +75 -0
  15. package/dist/types/markit/converters/pptx.d.ts +57 -0
  16. package/dist/types/markit/converters/xlsx.d.ts +25 -0
  17. package/dist/types/markit/index.d.ts +2 -0
  18. package/dist/types/markit/registry.d.ts +16 -0
  19. package/dist/types/markit/types.d.ts +30 -0
  20. package/dist/types/session/agent-session.d.ts +7 -8
  21. package/dist/types/session/auth-storage.d.ts +3 -2
  22. package/dist/types/session/yield-queue.d.ts +3 -1
  23. package/dist/types/tools/browser/attach.d.ts +1 -1
  24. package/dist/types/utils/markit.d.ts +0 -8
  25. package/dist/types/utils/mupdf-wasm-embed.d.ts +1 -0
  26. package/dist/types/utils/turndown.d.ts +15 -0
  27. package/dist/types/utils/zip.d.ts +119 -0
  28. package/package.json +20 -18
  29. package/scripts/build-binary.ts +7 -3
  30. package/scripts/bundle-dist.ts +28 -12
  31. package/scripts/embed-mupdf-wasm.ts +67 -0
  32. package/scripts/generate-docs-index.ts +48 -32
  33. package/scripts/omp +1 -1
  34. package/src/advisor/__tests__/advisor.test.ts +83 -0
  35. package/src/advisor/runtime.ts +16 -1
  36. package/src/cli/auth-broker-cli.ts +1 -3
  37. package/src/cli/auth-gateway-cli.ts +2 -5
  38. package/src/cli/update-cli.ts +63 -3
  39. package/src/config/model-discovery.ts +20 -8
  40. package/src/config/models-config-schema.ts +8 -1
  41. package/src/debug/index.ts +44 -0
  42. package/src/debug/remote-debugger.ts +151 -0
  43. package/src/debug/report-bundle.ts +2 -1
  44. package/src/internal-urls/docs-index.generated.txt +2 -0
  45. package/src/internal-urls/docs-index.ts +102 -0
  46. package/src/internal-urls/omp-protocol.ts +10 -9
  47. package/src/markit/NOTICE +32 -0
  48. package/src/markit/converters/docx.ts +56 -0
  49. package/src/markit/converters/epub.ts +136 -0
  50. package/src/markit/converters/mammoth.d.ts +24 -0
  51. package/src/markit/converters/pdf/columns.ts +103 -0
  52. package/src/markit/converters/pdf/extract.ts +574 -0
  53. package/src/markit/converters/pdf/grid.ts +780 -0
  54. package/src/markit/converters/pdf/headers.ts +106 -0
  55. package/src/markit/converters/pdf/index.ts +146 -0
  56. package/src/markit/converters/pdf/render.ts +501 -0
  57. package/src/markit/converters/pdf/types.ts +84 -0
  58. package/src/markit/converters/pptx.ts +325 -0
  59. package/src/markit/converters/xlsx.ts +173 -0
  60. package/src/markit/index.ts +2 -0
  61. package/src/markit/registry.ts +59 -0
  62. package/src/markit/types.ts +35 -0
  63. package/src/modes/components/snapcompact-shape-preview-doc.md +14 -7
  64. package/src/modes/components/snapcompact-shape-preview.ts +2 -2
  65. package/src/modes/controllers/input-controller.ts +29 -8
  66. package/src/modes/interactive-mode.ts +26 -9
  67. package/src/prompts/advisor/system.md +1 -0
  68. package/src/sdk.ts +5 -9
  69. package/src/session/agent-session.ts +75 -40
  70. package/src/session/auth-storage.ts +2 -11
  71. package/src/session/yield-queue.ts +7 -1
  72. package/src/slash-commands/builtin-registry.ts +1 -1
  73. package/src/tools/browser/attach.ts +2 -2
  74. package/src/tools/fetch.ts +25 -60
  75. package/src/tools/read.ts +1 -1
  76. package/src/tools/search.ts +1 -6
  77. package/src/tools/write.ts +25 -65
  78. package/src/utils/markit.ts +25 -9
  79. package/src/utils/mupdf-wasm-embed.ts +12 -0
  80. package/src/utils/tools-manager.ts +2 -11
  81. package/src/utils/turndown.ts +83 -0
  82. package/src/{tools/archive-reader.ts → utils/zip.ts} +453 -83
  83. package/src/web/scrapers/types.ts +3 -46
  84. package/dist/types/internal-urls/docs-index.generated.d.ts +0 -2
  85. package/dist/types/tools/archive-reader.d.ts +0 -49
  86. package/src/internal-urls/docs-index.generated.ts +0 -120
@@ -370,13 +370,64 @@ async function unlinkIfExists(filePath: string): Promise<void> {
370
370
  }
371
371
  }
372
372
 
373
+ /**
374
+ * Remove a backup binary without letting the removal abort a completed update.
375
+ *
376
+ * On Windows the executable that was just moved aside is still mapped as the
377
+ * running process image, so unlinking it fails with EPERM/EACCES until this
378
+ * process exits (issue #845). The replacement and verification already
379
+ * succeeded by the time we get here, so every error is swallowed; the leftover
380
+ * is reclaimed by {@link sweepStaleBackups} on the next update once it is no
381
+ * longer in use. Returns whether the file is gone.
382
+ */
383
+ async function removeBackupBestEffort(filePath: string): Promise<boolean> {
384
+ try {
385
+ await fs.promises.unlink(filePath);
386
+ return true;
387
+ } catch (err) {
388
+ return isEnoent(err);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Best-effort removal of binary-update backups left by earlier runs.
394
+ *
395
+ * Each self-update moves the previous executable to `<binary>.<timestamp>.<pid>.bak`
396
+ * before swapping the new one in. On Windows that backup cannot be deleted
397
+ * while the updating process is alive, so it is left for a later run to reclaim
398
+ * once its owning process has exited. Also matches the legacy fixed
399
+ * `<binary>.bak` name produced before backups were timestamped, so users
400
+ * upgrading from a buggy release get the orphaned file cleaned up.
401
+ */
402
+ export async function sweepStaleBackups(targetPath: string): Promise<void> {
403
+ const dir = path.dirname(targetPath);
404
+ const base = path.basename(targetPath);
405
+ let entries: string[];
406
+ try {
407
+ entries = await fs.promises.readdir(dir);
408
+ } catch {
409
+ return;
410
+ }
411
+ for (const entry of entries) {
412
+ if (!entry.startsWith(`${base}.`) || !entry.endsWith(".bak")) continue;
413
+ // Legacy "<base>.bak" → empty middle; new "<base>.<timestamp>.<pid>.bak"
414
+ // → dot-separated numeric run. Anything else is an unrelated *.bak file.
415
+ const middle = entry.slice(base.length + 1, entry.length - ".bak".length);
416
+ if (middle.length > 0 && !/^\d+(\.\d+)*$/.test(middle)) continue;
417
+ await removeBackupBestEffort(path.join(dir, entry));
418
+ }
419
+ }
420
+
373
421
  /**
374
422
  * Atomically replace the installed binary and roll back if version verification fails.
375
423
  */
376
424
  export async function replaceBinaryForUpdate(options: BinaryReplacementOptions): Promise<InstalledVersionVerification> {
377
425
  let backupReady = false;
378
426
  try {
379
- await unlinkIfExists(options.backupPath);
427
+ // `backupPath` is unique per attempt (see updateViaBinaryAt), so this rename
428
+ // never has to overwrite — or unlink — a possibly-locked leftover from an
429
+ // earlier run. Renaming the running executable itself is permitted on
430
+ // Windows; only deleting its still-mapped image is not.
380
431
  await fs.promises.rename(options.targetPath, options.backupPath);
381
432
  backupReady = true;
382
433
  await fs.promises.rename(options.tempPath, options.targetPath);
@@ -389,7 +440,10 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
389
440
  }
390
441
 
391
442
  backupReady = false;
392
- await unlinkIfExists(options.backupPath);
443
+ // Swap done and verified. On Windows the backup is still the running
444
+ // process image and cannot be unlinked until this process exits, so a
445
+ // failure here must NOT fail an otherwise-successful update.
446
+ await removeBackupBestEffort(options.backupPath);
393
447
  return verification;
394
448
  } catch (err) {
395
449
  if (backupReady) {
@@ -517,7 +571,11 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
517
571
  const url = `https://github.com/${REPO}/releases/download/${tag}/${binaryName}`;
518
572
 
519
573
  const tempPath = `${targetPath}.new`;
520
- const backupPath = `${targetPath}.bak`;
574
+ // Unique per attempt: a stale backup from an earlier update may still be
575
+ // locked (it is the previous process image on Windows), and a fixed name
576
+ // would force the move-aside rename to overwrite it. pid + timestamp keeps
577
+ // two forced updates in the same millisecond from colliding.
578
+ const backupPath = `${targetPath}.${Date.now()}.${process.pid}.bak`;
521
579
  console.log(chalk.dim(`Downloading ${binaryName}…`));
522
580
 
523
581
  const response = await fetch(url, { redirect: "follow" });
@@ -535,6 +593,8 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
535
593
  expectedVersion,
536
594
  verifyInstalledVersion,
537
595
  });
596
+ // Reclaim backups from earlier updates whose owning process has since exited.
597
+ await sweepStaleBackups(targetPath);
538
598
  printVerifiedVersion(expectedVersion);
539
599
  console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
540
600
  }
@@ -13,6 +13,7 @@ import {
13
13
  resolveModelReference,
14
14
  stripBracketedModelIdAffixes,
15
15
  } from "@oh-my-pi/pi-catalog/identity";
16
+ import { fetchLmStudioNativeModelMetadata } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
16
17
  import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
17
18
  import { isRecord } from "@oh-my-pi/pi-utils";
18
19
  import type { ProviderDiscovery } from "./models-config-schema";
@@ -379,18 +380,25 @@ export async function discoverOpenAIModelsList(
379
380
  const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
380
381
  let headers = baseHeaders;
381
382
  const attempt = async (h: Record<string, string>) => {
382
- const res = await ctx.fetch(modelsUrl, {
383
- headers: h,
384
- signal: AbortSignal.timeout(10_000),
385
- });
383
+ const nativeMetadataPromise =
384
+ providerConfig.discovery.type === "lm-studio"
385
+ ? fetchLmStudioNativeModelMetadata(baseUrl, ctx.fetch, { headers: h })
386
+ : Promise.resolve(null);
387
+ const [res, nativeMetadata] = await Promise.all([
388
+ ctx.fetch(modelsUrl, {
389
+ headers: h,
390
+ signal: AbortSignal.timeout(10_000),
391
+ }),
392
+ nativeMetadataPromise,
393
+ ]);
386
394
  if (!res.ok) {
387
395
  throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
388
396
  }
389
397
  headers = h;
390
- return res;
398
+ return [res, nativeMetadata] as const;
391
399
  };
392
400
  const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
393
- const response = apiKey
401
+ const [response, nativeMetadata] = apiKey
394
402
  ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
395
403
  : await attempt(baseHeaders);
396
404
  const payload = (await response.json()) as {
@@ -401,8 +409,12 @@ export async function discoverOpenAIModelsList(
401
409
  for (const item of models) {
402
410
  const id = item.id;
403
411
  if (!id) continue;
412
+ const nativeMetadataForModel = nativeMetadata?.get(id);
404
413
  const contextWindow =
405
- toPositiveNumberOrUndefined(item.max_model_len) ?? toPositiveNumberOrUndefined(item.context_length) ?? 128000;
414
+ toPositiveNumberOrUndefined(item.max_model_len) ??
415
+ toPositiveNumberOrUndefined(item.context_length) ??
416
+ nativeMetadataForModel?.contextWindow ??
417
+ 128000;
406
418
  discovered.push(
407
419
  buildModel({
408
420
  id,
@@ -411,7 +423,7 @@ export async function discoverOpenAIModelsList(
411
423
  provider: providerConfig.provider,
412
424
  baseUrl,
413
425
  reasoning: false,
414
- input: ["text"],
426
+ input: nativeMetadataForModel?.input ?? ["text"],
415
427
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
416
428
  contextWindow,
417
429
  maxTokens: Math.min(contextWindow, discoveryDefaultMaxTokens(providerConfig.api)),
@@ -1,4 +1,11 @@
1
- import { type } from "arktype";
1
+ import { scope } from "arktype";
2
+
3
+ // Config schemas validate at most a handful of times per process (on config
4
+ // load), so the eager JIT codegen ArkType runs at definition time is pure
5
+ // startup tax. A local jitless scope skips that codegen and falls back to
6
+ // interpreted traversal — ~65% cheaper to construct, validation correctness
7
+ // unchanged. (No `name`: duplicate module instances would collide.)
8
+ const { type } = scope({}, { jitless: true });
2
9
 
3
10
  const OpenRouterRoutingSchema = type({
4
11
  "only?": "string[]",
@@ -29,6 +29,7 @@ import { generateHeapSnapshotData, type ProfilerSession, startCpuProfile } from
29
29
  import { buildSampleImage, ProtocolProbeComponent } from "./protocol-probe";
30
30
  import { RawSseViewerComponent } from "./raw-sse";
31
31
  import { resolveRawSseDebugBuffer } from "./raw-sse-buffer";
32
+ import { getRemoteDebugger, type RemoteDebuggerInfo, startRemoteDebuggerServer } from "./remote-debugger";
32
33
  import { clearArtifactCache, createDebugLogSource, createReportBundle, getArtifactCacheStats } from "./report-bundle";
33
34
  import { collectSystemInfo, formatSystemInfo } from "./system-info";
34
35
  import { collectTerminalState, formatTerminalState } from "./terminal-info";
@@ -49,6 +50,11 @@ const DEBUG_MENU_ITEMS: SelectItem[] = [
49
50
  description: "Styling, links, text sizing, graphics, notify",
50
51
  },
51
52
  { value: "raw-sse", label: "View: raw SSE stream", description: "Show live provider SSE frames" },
53
+ {
54
+ value: "remote-debugger",
55
+ label: "Start: JS remote debugger",
56
+ description: "Expose JavaScriptCore inspector socket (experimental)",
57
+ },
52
58
  {
53
59
  value: "transcript",
54
60
  label: "Export: TUI transcript",
@@ -122,6 +128,9 @@ export class DebugSelectorComponent extends Container {
122
128
  case "raw-sse":
123
129
  await this.#handleViewRawSse();
124
130
  break;
131
+ case "remote-debugger":
132
+ await this.#handleStartRemoteDebugger();
133
+ break;
125
134
  case "system":
126
135
  await this.#handleViewSystemInfo();
127
136
  break;
@@ -353,6 +362,41 @@ export class DebugSelectorComponent extends Container {
353
362
  this.ctx.ui.requestRender();
354
363
  }
355
364
 
365
+ async #handleStartRemoteDebugger(): Promise<void> {
366
+ const existing = getRemoteDebugger();
367
+ let info: RemoteDebuggerInfo;
368
+ try {
369
+ info = existing ?? (await startRemoteDebuggerServer());
370
+ } catch (err) {
371
+ this.ctx.showError(`Failed to start remote debugger: ${err instanceof Error ? err.message : String(err)}`);
372
+ return;
373
+ }
374
+
375
+ const block = new TranscriptBlock();
376
+ block.addChild(
377
+ new Text(
378
+ theme.fg(
379
+ "success",
380
+ `${theme.status.success} JavaScriptCore remote inspector ${existing ? "already running" : "started"}`,
381
+ ),
382
+ 1,
383
+ 0,
384
+ ),
385
+ );
386
+ block.addChild(new Text(theme.fg("dim", `Listening on ${info.host}:${info.port}`), 1, 0));
387
+ block.addChild(
388
+ new Text(
389
+ theme.fg(
390
+ "muted",
391
+ "Experimental WebKit RemoteInspectorServer socket (Bun marks it untested on macOS). One-way for this process — there is no stop. Attach a compatible WebKit/Safari Web Inspector client.",
392
+ ),
393
+ 1,
394
+ 0,
395
+ ),
396
+ );
397
+ this.ctx.present(block);
398
+ }
399
+
356
400
  async #handleViewSystemInfo(): Promise<void> {
357
401
  try {
358
402
  const info = await collectSystemInfo();
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Bun JavaScriptCore remote inspector control.
3
+ *
4
+ * Wraps `bun:jsc`'s `startRemoteDebugger`, which exposes JavaScriptCore's
5
+ * built-in WebKit RemoteInspectorServer over a raw socket. The API is one-shot
6
+ * and rough around the edges (Bun documents it as untested, "may not be
7
+ * supported yet on macOS"):
8
+ * - it returns `void` and has no stop handle, so we track the live endpoint
9
+ * at module scope and make starting idempotent;
10
+ * - it rejects port `0`, so "let the OS pick" is implemented by reserving a
11
+ * free port via `node:net` and handing the concrete number to Bun;
12
+ * - on macOS (Bun 1.3.x) it throws a spurious "port already in use" error
13
+ * even when the server binds fine, so success is decided by a loopback
14
+ * probe rather than by whether the call threw.
15
+ */
16
+
17
+ import { startRemoteDebugger } from "bun:jsc";
18
+ import * as net from "node:net";
19
+
20
+ const DEFAULT_HOST = "127.0.0.1";
21
+ /** How long to keep probing for the inspector socket before giving up. */
22
+ const PROBE_DEADLINE_MS = 1000;
23
+ const PROBE_INTERVAL_MS = 50;
24
+
25
+ export interface RemoteDebuggerInfo {
26
+ host: string;
27
+ port: number;
28
+ }
29
+
30
+ let active: RemoteDebuggerInfo | null = null;
31
+ /** In-flight start, shared so concurrent callers coalesce onto one launch. */
32
+ let starting: Promise<RemoteDebuggerInfo> | null = null;
33
+
34
+ /** Underlying starter signature; tests inject a disposable listener in its place. */
35
+ export type RemoteDebuggerStarter = (host: string, port: number) => void;
36
+
37
+ export interface StartRemoteDebuggerOptions {
38
+ /** Explicit port; when omitted a free port is reserved automatically. */
39
+ port?: number;
40
+ /** Override the JSC starter. Defaults to `bun:jsc`'s `startRemoteDebugger`. */
41
+ start?: RemoteDebuggerStarter;
42
+ }
43
+
44
+ /** The live inspector endpoint for this process, or `null` if not started. */
45
+ export function getRemoteDebugger(): RemoteDebuggerInfo | null {
46
+ return active;
47
+ }
48
+
49
+ /** Reserve a free TCP port on `host` by binding to `0`, then releasing it. */
50
+ async function reserveFreePort(host: string): Promise<number> {
51
+ const server = net.createServer();
52
+ const listening = Promise.withResolvers<number>();
53
+ server.once("error", listening.reject);
54
+ server.listen(0, host, () => {
55
+ const addr = server.address();
56
+ if (addr && typeof addr === "object") listening.resolve(addr.port);
57
+ else listening.reject(new Error("Failed to reserve a debugger port"));
58
+ });
59
+ try {
60
+ return await listening.promise;
61
+ } finally {
62
+ const closed = Promise.withResolvers<void>();
63
+ server.close(() => closed.resolve());
64
+ await closed.promise;
65
+ }
66
+ }
67
+
68
+ /** Resolve once `host:port` accepts a TCP connection (within one attempt). */
69
+ function tryConnect(host: string, port: number, timeoutMs: number): Promise<boolean> {
70
+ const { promise, resolve } = Promise.withResolvers<boolean>();
71
+ const socket = net.createConnection({ host, port });
72
+ let settled = false;
73
+ const finish = (ok: boolean) => {
74
+ if (settled) return;
75
+ settled = true;
76
+ socket.destroy();
77
+ resolve(ok);
78
+ };
79
+ socket.setTimeout(timeoutMs);
80
+ socket.once("connect", () => finish(true));
81
+ socket.once("timeout", () => finish(false));
82
+ socket.once("error", () => finish(false));
83
+ return promise;
84
+ }
85
+
86
+ /** Poll the inspector socket until it accepts a connection or the deadline passes. */
87
+ async function waitForListening(host: string, port: number): Promise<boolean> {
88
+ const deadline = Date.now() + PROBE_DEADLINE_MS;
89
+ do {
90
+ if (await tryConnect(host, port, PROBE_INTERVAL_MS)) return true;
91
+ await Bun.sleep(PROBE_INTERVAL_MS);
92
+ } while (Date.now() < deadline);
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Start the JavaScriptCore remote inspector for this process and return its
98
+ * endpoint. Idempotent: the underlying API cannot be stopped or rebound, so a
99
+ * second call returns the existing endpoint instead of starting again. When
100
+ * `port` is omitted a free port is reserved automatically.
101
+ *
102
+ * Throws only when the socket never comes up; Bun's spurious bind error is
103
+ * swallowed and overridden by the loopback probe.
104
+ */
105
+ export async function startRemoteDebuggerServer(options: StartRemoteDebuggerOptions = {}): Promise<RemoteDebuggerInfo> {
106
+ if (active) return active;
107
+ starting ??= launch(options);
108
+ try {
109
+ return await starting;
110
+ } finally {
111
+ starting = null;
112
+ }
113
+ }
114
+
115
+ async function launch({ port, start = startRemoteDebugger }: StartRemoteDebuggerOptions): Promise<RemoteDebuggerInfo> {
116
+ const host = DEFAULT_HOST;
117
+ const chosen = port ?? (await reserveFreePort(host));
118
+
119
+ // Something already on this port? Refuse up front: otherwise Bun throws a
120
+ // real bind error and our success probe would connect to that unrelated
121
+ // service, marking a bogus endpoint as the debugger.
122
+ if (await tryConnect(host, chosen, PROBE_INTERVAL_MS)) {
123
+ throw new Error(`Port ${host}:${chosen} is already in use; cannot start remote debugger there.`);
124
+ }
125
+
126
+ let thrown: unknown;
127
+ try {
128
+ start(host, chosen);
129
+ } catch (err) {
130
+ // Bun's startRemoteDebugger throws a spurious bind error even on success,
131
+ // so defer the verdict to the loopback probe below.
132
+ thrown = err;
133
+ }
134
+
135
+ if (await waitForListening(host, chosen)) {
136
+ active = { host, port: chosen };
137
+ return active;
138
+ }
139
+
140
+ throw thrown instanceof Error ? thrown : new Error(`Remote debugger socket never came up on ${host}:${chosen}`);
141
+ }
142
+
143
+ /**
144
+ * Test-only: forget the tracked endpoint so a fresh start can be exercised.
145
+ * Does not (and cannot) stop a real JSC inspector — callers in tests own the
146
+ * disposable listener they injected.
147
+ */
148
+ export function __resetRemoteDebuggerForTests(): void {
149
+ active = null;
150
+ starting = null;
151
+ }
@@ -7,6 +7,7 @@ import * as fs from "node:fs/promises";
7
7
  import * as path from "node:path";
8
8
  import type { WorkProfile } from "@oh-my-pi/pi-natives";
9
9
  import { APP_NAME, getLogPath, getLogsDir, getReportsDir, isEnoent } from "@oh-my-pi/pi-utils";
10
+ import { writeArchive } from "../utils/zip";
10
11
  import type { CpuProfile, HeapSnapshot } from "./profiler";
11
12
  import { collectSystemInfo, sanitizeEnv } from "./system-info";
12
13
 
@@ -165,7 +166,7 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
165
166
  }
166
167
 
167
168
  // Write archive
168
- await Bun.Archive.write(outputPath, data, { compress: "gzip" });
169
+ await writeArchive(outputPath, "tar.gz", Object.entries(data));
169
170
 
170
171
  return { path: outputPath, files };
171
172
  }