@pasko70/pibo 1.8.2 → 1.9.2

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 (40) hide show
  1. package/dist/apps/chat/data/chat-data-mappers.js +8 -0
  2. package/dist/apps/chat/data/project-service.js +23 -0
  3. package/dist/apps/chat/static-assets.js +6 -5
  4. package/dist/apps/chat-ui/assets/{dist--lraqdDn.js → dist-2KdPXbMT.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-Dgz3iWay.js → dist-5GM30SQK.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-CMFjl7MX.js → dist-9lsp1UpA.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-6pcjNbPQ.js → dist-BWbWIOcD.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-COlFTw2x.js → dist-BYKMZlI0.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-IlQbTNzw.js → dist-C-5u2QIS.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-bLd-NaVv.js → dist-Cge8JklW.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-CoCvyd4f.js → dist-CjYtD7ZT.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-CZSEM5So.js → dist-DFhhiR8M.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-YdEosvsr.js → dist-Etxmpyxg.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-CajLLpaw.js → dist-Idz5kzy8.js} +1 -1
  15. package/dist/apps/chat-ui/assets/index-BqD_bm7z.js +173 -0
  16. package/dist/apps/chat-ui/assets/index-C0x9nEcf.css +1 -0
  17. package/dist/apps/chat-ui/index.html +2 -2
  18. package/dist/apps/chat-vscode-web/assets/index-Cst9OUkC.js +41 -0
  19. package/dist/apps/chat-vscode-web/index.html +1 -1
  20. package/dist/cli.js +17 -0
  21. package/dist/compute/resource-health.js +12 -11
  22. package/dist/gateway/server.js +14 -1
  23. package/dist/gateway/web.js +2 -1
  24. package/dist/plugins/context-files.js +4 -2
  25. package/dist/ralph/store.js +1 -1
  26. package/dist/resources/cli.js +135 -0
  27. package/dist/resources/lifecycle.js +353 -0
  28. package/dist/resources/reaper-state.js +94 -0
  29. package/dist/resources/reaper.js +145 -0
  30. package/dist/session-ui/activeTurn.js +126 -0
  31. package/dist/session-ui/delegation.js +89 -0
  32. package/dist/session-ui/index.js +1 -0
  33. package/dist/session-ui/terminalRows.js +61 -0
  34. package/dist/shared/trace-engine.js +3 -2
  35. package/dist/shared/trace-event-projection.js +124 -14
  36. package/dist/shared/trace-transcript.js +73 -5
  37. package/package.json +1 -1
  38. package/dist/apps/chat-ui/assets/index-DbRZGRDd.css +0 -1
  39. package/dist/apps/chat-ui/assets/index-HPWlrJwv.js +0 -173
  40. package/dist/apps/chat-vscode-web/assets/index-Dge6XEYB.js +0 -41
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-Dge6XEYB.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-Cst9OUkC.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-B5QK07zO.css">
10
10
  </head>
11
11
  <body>
package/dist/cli.js CHANGED
@@ -115,6 +115,11 @@ export async function runPiboCli(argv = process.argv) {
115
115
  await runComputeCli([argv[0] ?? "node", "pibo compute", ...argv.slice(3)]);
116
116
  return;
117
117
  }
118
+ if (argv[2] === "resources") {
119
+ const { runResourcesCli } = await import("./resources/cli.js");
120
+ await runResourcesCli([argv[0] ?? "node", "pibo resources", ...argv.slice(3)]);
121
+ return;
122
+ }
118
123
  if (argv[2] === "setup") {
119
124
  const { runSetupCli } = await import("./setup/cli.js");
120
125
  await runSetupCli([argv[0] ?? "node", "pibo setup", ...argv.slice(3)]);
@@ -217,6 +222,17 @@ export async function runPiboCli(argv = process.argv) {
217
222
  const { runComputeCli } = await import("./compute/cli.js");
218
223
  await runComputeCli([argv[0] ?? "node", "pibo compute", ...args]);
219
224
  });
225
+ program
226
+ .command("resources")
227
+ .description("Inspect and safely reap managed compute and browser resources")
228
+ .helpOption(false)
229
+ .allowUnknownOption(true)
230
+ .allowExcessArguments(true)
231
+ .argument("[args...]")
232
+ .action(async (args) => {
233
+ const { runResourcesCli } = await import("./resources/cli.js");
234
+ await runResourcesCli([argv[0] ?? "node", "pibo resources", ...args]);
235
+ });
220
236
  program
221
237
  .command("setup")
222
238
  .description("Plan user-host installs and developer-host upgrades")
@@ -439,6 +455,7 @@ Commands:
439
455
  debug Inspect local Pibo data
440
456
  data Inspect and maintain Pibo data stores
441
457
  compute Manage Pibo Docker compute workers
458
+ resources Inspect and safely reap managed compute and browser resources
442
459
  setup Plan user-host installs and developer-host upgrades
443
460
  skills Manage Pibo user skills
444
461
  cron Manage scheduled Pibo jobs
@@ -4,6 +4,7 @@ import { readdir, readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, join } from "node:path";
6
6
  import { promisify } from "node:util";
7
+ import { readResourceReaperTimerStatus } from "../resources/reaper-state.js";
7
8
  import { createEmptyBrowserPoolState, normalizeBrowserPoolState } from "../tools/browser-pool.js";
8
9
  import { getComputeDiskDiagnostics, listWorkers } from "./docker.js";
9
10
  const execFileAsync = promisify(execFile);
@@ -13,7 +14,9 @@ export function parseProcessList(output) {
13
14
  for (const line of output.split("\n")) {
14
15
  if (!line.trim())
15
16
  continue;
16
- const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
17
+ const withElapsed = line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
18
+ const legacy = withElapsed ? undefined : line.match(/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
19
+ const match = withElapsed ?? legacy;
17
20
  if (!match)
18
21
  continue;
19
22
  const pid = Number(match[1]);
@@ -21,10 +24,11 @@ export function parseProcessList(output) {
21
24
  const pgid = Number(match[3]);
22
25
  if (!Number.isInteger(pid) || !Number.isInteger(ppid) || !Number.isInteger(pgid))
23
26
  continue;
24
- const commandName = match[4] ?? "";
25
- const args = match[5] ?? "";
27
+ const elapsedSeconds = withElapsed ? Number(match[4]) : undefined;
28
+ const commandName = withElapsed ? match[5] ?? "" : match[4] ?? "";
29
+ const args = withElapsed ? match[6] ?? "" : match[5] ?? "";
26
30
  const isChromium = isChromiumCommand(commandName, args);
27
- processes.push({ pid, ppid, pgid, commandName, args, isChromium, isMainProcess: isChromium && !/\s--type=/.test(` ${args}`) });
31
+ processes.push({ pid, ppid, pgid, elapsedSeconds, commandName, args, isChromium, isMainProcess: isChromium && !/\s--type=/.test(` ${args}`) });
28
32
  }
29
33
  return processes;
30
34
  }
@@ -160,6 +164,7 @@ function describeUnassignedBrowserProcess(process, workers) {
160
164
  workerName: worker?.name,
161
165
  userDataDir,
162
166
  cdpPort,
167
+ elapsedSeconds: process.elapsedSeconds,
163
168
  argsPreview: sanitizeArgsPreview(process.args),
164
169
  nextCommands,
165
170
  };
@@ -242,7 +247,7 @@ async function collectWorkers() {
242
247
  }
243
248
  async function collectProcesses() {
244
249
  try {
245
- const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,pgid=,comm=,args="], { maxBuffer: 10 * 1024 * 1024 });
250
+ const { stdout } = await execFileAsync("ps", ["-eo", "pid=,ppid=,pgid=,etimes=,comm=,args="], { maxBuffer: 10 * 1024 * 1024 });
246
251
  return { processes: parseProcessList(stdout) };
247
252
  }
248
253
  catch (error) {
@@ -290,13 +295,9 @@ async function findStateJsonFiles(rootDir) {
290
295
  }
291
296
  function detectReaperTimerStatus() {
292
297
  if (process.env.PIBO_RESOURCE_REAPER_TIMER_STATUS === "configured") {
293
- return { status: "configured", details: "Resource reaper timer is marked configured by PIBO_RESOURCE_REAPER_TIMER_STATUS.", nextCommands: ["pibo compute health --json"] };
298
+ return { status: "configured", details: "Resource reaper timer is marked configured by PIBO_RESOURCE_REAPER_TIMER_STATUS.", nextCommands: ["pibo resources status --json"] };
294
299
  }
295
- return {
296
- status: "missing",
297
- details: "No automatic resource reaper timer status was found; keep using read-only diagnostics and dry-run cleanup until timers are explicitly enabled.",
298
- nextCommands: ["pibo compute health --json", "pibo tools browser-use pool reap --json", "pibo compute reap --dry-run --json"],
299
- };
300
+ return readResourceReaperTimerStatus();
300
301
  }
301
302
  function browserProcessMatchesPool(process, state) {
302
303
  if (state.pid && process.pid === state.pid)
@@ -2,6 +2,7 @@ import { createServer } from "node:net";
2
2
  import { createDefaultPiboPluginRegistry, createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault } from "../plugins/builtin.js";
3
3
  import { PiboSessionRouter } from "../core/session-router.js";
4
4
  import { loadPiboModelDefaults, selectRequestedModelProfile } from "../core/model-defaults.js";
5
+ import { ResourceReaperService } from "../resources/reaper.js";
5
6
  import { DEFAULT_GATEWAY_HOST, DEFAULT_GATEWAY_PORT, encodeFrame, errorResponse, isGatewayRequestFrame, isGatewaySubscribeFrame, } from "./protocol.js";
6
7
  import { releaseFallbackGatewayPid, releaseGatewayPid, writeFallbackGatewayPid, writeGatewayPid } from "./pidfile.js";
7
8
  const DEFAULT_MAX_BACKPRESSURE_FRAMES = 1_000;
@@ -131,6 +132,7 @@ export class PiboGatewayServer {
131
132
  closedSlowClients = 0;
132
133
  server;
133
134
  unsubscribe;
135
+ resourceReaper;
134
136
  constructor(options = {}) {
135
137
  this.options = options;
136
138
  this.pluginRegistry = options.pluginRegistry ?? createDefaultPiboPluginRegistry();
@@ -159,8 +161,14 @@ export class PiboGatewayServer {
159
161
  if (this.options.startChannels !== false) {
160
162
  await this.startChannels();
161
163
  }
164
+ if (this.options.resourceReaper) {
165
+ this.resourceReaper = new ResourceReaperService(this.options.resourceReaper);
166
+ await this.resourceReaper.start();
167
+ }
162
168
  }
163
169
  async stop() {
170
+ await this.resourceReaper?.stop();
171
+ this.resourceReaper = undefined;
164
172
  await this.stopChannels();
165
173
  await this.pluginRegistry.getAuthService()?.stop?.();
166
174
  this.unsubscribe?.();
@@ -341,6 +349,11 @@ export class PiboGatewayServer {
341
349
  return this.sessionStore;
342
350
  }
343
351
  }
352
+ export function resolveGatewayResourceReaperOptions(options) {
353
+ if (options.resourceReaper === false || process.env.PIBO_RESOURCE_REAPER_DISABLED === "1")
354
+ return false;
355
+ return options.resourceReaper ?? {};
356
+ }
344
357
  export async function runGatewayServer(options = {}) {
345
358
  const fallbackMode = process.env.PIBO_FALLBACK_MODE === "1";
346
359
  try {
@@ -357,7 +370,7 @@ export async function runGatewayServer(options = {}) {
357
370
  const releasePid = fallbackMode ? releaseFallbackGatewayPid : releaseGatewayPid;
358
371
  let server;
359
372
  try {
360
- server = new PiboGatewayServer(options);
373
+ server = new PiboGatewayServer({ ...options, resourceReaper: resolveGatewayResourceReaperOptions(options) });
361
374
  await server.start();
362
375
  }
363
376
  catch (error) {
@@ -12,7 +12,7 @@ import { PiboPluginRegistry } from "../plugins/registry.js";
12
12
  import { createPiboWebHostPlugin } from "../plugins/web.js";
13
13
  import { DEFAULT_WEB_CHANNEL_HOST, DEFAULT_WEB_CHANNEL_PORT } from "../web/channel.js";
14
14
  import { loadPiboConfig } from "../config/config.js";
15
- import { PiboGatewayServer } from "./server.js";
15
+ import { PiboGatewayServer, resolveGatewayResourceReaperOptions } from "./server.js";
16
16
  import { releaseFallbackGatewayPid, releaseGatewayPid, writeFallbackGatewayPid, writeGatewayPid } from "./pidfile.js";
17
17
  const PUBLIC_WEB_CHANNEL_HOST = "0.0.0.0";
18
18
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
@@ -196,6 +196,7 @@ export async function runWebGatewayServer(options = {}) {
196
196
  server = new PiboGatewayServer({
197
197
  ...resolvedOptions,
198
198
  pluginRegistry,
199
+ resourceReaper: resolveGatewayResourceReaperOptions(resolvedOptions),
199
200
  });
200
201
  await server.start();
201
202
  }
@@ -362,12 +362,13 @@ class ContextFileService {
362
362
  const markdown = normalizeMarkdown(body.markdown ?? "");
363
363
  const scope = normalizeScope(body.scope);
364
364
  const agentProfileName = normalizeAgentProfileName(body.agentProfileName, scope === "agent");
365
+ const key = uniqueKey(`ctx:${slugSegment(label)}`, new Set(this.list(context).map((file) => file.key)));
365
366
  const targetDir = this.resolveManagedDir(scope, agentProfileName);
366
367
  const absolutePath = uniquePath(targetDir, managedFileName(label));
367
368
  await mkdir(dirname(absolutePath), { recursive: true });
368
369
  await writeFile(absolutePath, markdown, "utf8");
369
370
  const record = this.store.createFile({
370
- key: uniqueKey(`ctx:${slugSegment(label)}`, new Set(this.list(context).map((file) => file.key))),
371
+ key,
371
372
  label,
372
373
  managedPath: absolutePath,
373
374
  scope,
@@ -398,12 +399,13 @@ class ContextFileService {
398
399
  const label = normalizeOptionalLabel(body.label) ?? sourceFile.label ?? sourceKey;
399
400
  const scope = normalizeScope(body.scope, "global");
400
401
  const agentProfileName = normalizeAgentProfileName(body.agentProfileName, scope === "agent");
402
+ const key = uniqueKey(`ctx:${slugSegment(label)}`, new Set(this.list(context).map((file) => file.key)));
401
403
  const targetDir = this.resolveManagedDir(scope, agentProfileName);
402
404
  const absolutePath = uniquePath(targetDir, managedFileName(label));
403
405
  await mkdir(dirname(absolutePath), { recursive: true });
404
406
  await writeFile(absolutePath, sourceDescriptor.content ?? "", "utf8");
405
407
  const record = this.store.createFile({
406
- key: uniqueKey(`ctx:${slugSegment(label)}`, new Set(this.list(context).map((file) => file.key))),
408
+ key,
407
409
  label,
408
410
  managedPath: absolutePath,
409
411
  scope,
@@ -340,7 +340,7 @@ export class PiboRalphStore {
340
340
  const reachedMaxIterations = job.maxIterations !== undefined && completedIterations >= job.maxIterations;
341
341
  const shouldDisable = reachedMaxIterations || input.stopAfterRun === true || input.stopEvaluation?.finalAction === 'stop-after-run' || input.stopEvaluation?.finalAction === 'cancel-current-run';
342
342
  const state = { ...job.state, runningAt: undefined, completedIterations, lastRunAt: timestamp, lastRunId: input.runId, lastStatus: input.status === 'error' ? 'error' : input.status === 'cancelled' ? 'cancelled' : 'ok', lastError: input.error, lastPiboSessionId: input.piboSessionId ?? job.state.lastPiboSessionId, consecutiveErrors: input.status === 'error' ? (job.state.consecutiveErrors ?? 0) + 1 : 0, conditionStates: input.conditionStates ?? job.state.conditionStates, lastStopEvaluation: input.stopEvaluation ?? job.state.lastStopEvaluation };
343
- this.db.prepare('UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = ?, reason = ?, error = ?, completed_at = ?, updated_at = ? WHERE id = ?').run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, timestamp, timestamp, input.runId);
343
+ this.db.prepare('UPDATE pibo_ralph_runs SET status = ?, pibo_session_id = COALESCE(?, pibo_session_id), reason = ?, error = ?, completed_at = ?, updated_at = ? WHERE id = ?').run(input.status, input.piboSessionId ?? null, input.reason ?? input.stopEvaluation?.reason ?? null, input.error ?? null, timestamp, timestamp, input.runId);
344
344
  this.db.prepare('UPDATE pibo_ralph_jobs SET enabled = ?, state_json = ?, updated_at = ? WHERE id = ?').run(shouldDisable ? 0 : job.enabled ? 1 : 0, JSON.stringify(state), timestamp, job.id);
345
345
  }
346
346
  appendRunFact(input) {
@@ -0,0 +1,135 @@
1
+ import { Command } from "commander";
2
+ import { getComputeResourceHealth } from "../compute/resource-health.js";
3
+ import { renderComputeResourceHealthText } from "../compute/cli.js";
4
+ import { applyResourceReapPlan, getActiveResourceLeases, planResourceReap, } from "./lifecycle.js";
5
+ function printJson(value) {
6
+ console.log(JSON.stringify(value, null, 2));
7
+ }
8
+ function parseNonNegativeNumber(value) {
9
+ const parsed = Number(value);
10
+ if (!Number.isFinite(parsed) || parsed < 0)
11
+ throw new Error("Value must be a non-negative number");
12
+ return parsed;
13
+ }
14
+ function parsePidList(value) {
15
+ const pids = value.split(",").map((item) => Number.parseInt(item.trim(), 10));
16
+ if (pids.some((pid) => !Number.isInteger(pid) || pid <= 0))
17
+ throw new Error("PIDs must be positive integers separated by commas");
18
+ return [...new Set(pids)];
19
+ }
20
+ export function renderResourceLeasesText(leases) {
21
+ if (leases.length === 0)
22
+ return "No active managed browser-pool leases.\nNext: pibo resources status";
23
+ const lines = ["LEASE_ID\tHOLDER\tWORKER/POOL\tEXPIRY\tSTATE"];
24
+ for (const lease of leases) {
25
+ lines.push(`${lease.leaseId}\t${lease.holder ?? "-"}\t${lease.workerId}/${lease.poolId}\t${lease.expiresAt ?? "-"}\t${lease.state}`);
26
+ }
27
+ lines.push("Next: pibo resources status");
28
+ return lines.join("\n");
29
+ }
30
+ export function renderResourceReapText(value) {
31
+ const applied = "applied" in value;
32
+ const plan = applied ? value.plan : value;
33
+ const lines = [
34
+ `Resource reap ${applied ? "apply" : "dry-run"}: ${plan.browserPools.selected} browser pool(s), ${plan.unmanagedBrowsers.selected} unmanaged browser process group(s), ${plan.staleFiles.selected} stale pid/port file(s), ${plan.compute.summary.selected} compute worker(s) selected`,
35
+ "BROWSER_ACTION\tWORKER/POOL OR PID/PGID\tREASON",
36
+ ];
37
+ for (const item of plan.browserPools.items)
38
+ lines.push(`${item.action}\t${item.workerId}/${item.poolId}\t${item.reason}`);
39
+ for (const item of plan.unmanagedBrowsers.items)
40
+ lines.push(`${item.action}\t${item.pid}/${item.processGroupId}\t${item.reason}`);
41
+ if (applied) {
42
+ lines.push(`Reaped browser pools: ${value.browserResults.filter((result) => result.reaped).length}`);
43
+ lines.push(`Terminated unmanaged browser process groups: ${value.terminatedUnmanagedBrowsers.join(", ") || "none"}`);
44
+ lines.push(`Removed stale pid/port files: ${value.removedStaleFiles.length}`);
45
+ lines.push(`Removed compute workers: ${value.removedComputeWorkers.join(", ") || "none"}`);
46
+ }
47
+ else {
48
+ const args = [
49
+ "pibo resources reap --apply",
50
+ `--max-age-minutes ${plan.options.maxAgeMinutes}`,
51
+ `--idle-timeout-minutes ${plan.options.idleTimeoutMinutes}`,
52
+ `--unmanaged-browser-grace-minutes ${plan.options.unmanagedBrowserGraceMinutes}`,
53
+ ];
54
+ if (plan.options.includeDev)
55
+ args.push("--include-dev");
56
+ if (plan.options.browserPoolRoot)
57
+ args.push(`--browser-pool-root ${plan.options.browserPoolRoot}`);
58
+ if (plan.options.browserUseHome)
59
+ args.push(`--browser-use-home ${plan.options.browserUseHome}`);
60
+ lines.push(`Dry-run only. Apply after review with: ${args.join(" ")}`);
61
+ }
62
+ lines.push("Worktrees are always preserved.");
63
+ return lines.join("\n");
64
+ }
65
+ export function serializeResourceStatus(health) {
66
+ return {
67
+ ...health,
68
+ nextCommands: [...new Set([
69
+ "pibo resources status --json",
70
+ "pibo resources leases --json",
71
+ "pibo resources reap --dry-run --json",
72
+ ...health.nextCommands,
73
+ ])],
74
+ };
75
+ }
76
+ export async function runResourcesCli(argv) {
77
+ const program = new Command();
78
+ program
79
+ .name("pibo resources")
80
+ .description("Inspect and safely reap managed compute and browser resources")
81
+ .helpOption("-h, --help", "Show help")
82
+ .showHelpAfterError()
83
+ .helpCommand("help [command]", "Show help for command")
84
+ .addHelpText("after", "\nNext:\n pibo resources status\n pibo resources leases\n pibo resources reap --help\n");
85
+ program
86
+ .command("status")
87
+ .alias("doctor")
88
+ .description("Show read-only aggregate compute and browser health")
89
+ .option("--browser-pool-root <path>", "Browser pool root directory to scan")
90
+ .option("--browser-use-home <path>", "Browser-use home directory to scan for stale CDP files")
91
+ .option("--json", "Print machine-readable resource health")
92
+ .action(async (options) => {
93
+ const health = serializeResourceStatus(await getComputeResourceHealth(options));
94
+ if (options.json)
95
+ printJson(health);
96
+ else
97
+ console.log(renderComputeResourceHealthText(health).replace(/^Compute resource health:/, "Resource status:"));
98
+ });
99
+ program
100
+ .command("leases")
101
+ .description("List active managed browser-pool leases")
102
+ .option("--browser-pool-root <path>", "Browser pool root directory to scan")
103
+ .option("--json", "Print machine-readable leases")
104
+ .action(async (options) => {
105
+ const leases = await getActiveResourceLeases(options.browserPoolRoot);
106
+ if (options.json)
107
+ printJson({ leases });
108
+ else
109
+ console.log(renderResourceLeasesText(leases));
110
+ });
111
+ program
112
+ .command("reap")
113
+ .description("Preview or apply aggregate browser and compute cleanup")
114
+ .option("--dry-run", "Preview cleanup without changing resources (default)")
115
+ .option("--apply", "Apply cleanup after rechecking current resource safety")
116
+ .option("--include-dev", "Also select eligible dev compute workers")
117
+ .option("--max-age-minutes <n>", "Select compute workers older than this many minutes", parseNonNegativeNumber, 60)
118
+ .option("--idle-timeout-minutes <n>", "Select browser pools idle for this many minutes", parseNonNegativeNumber, 10)
119
+ .option("--unmanaged-browser-grace-minutes <n>", "Select unmanaged Chromium older than this many minutes", parseNonNegativeNumber, 10)
120
+ .option("--exempt-browser-pids <list>", "Comma-separated browser PIDs or process groups to preserve", parsePidList)
121
+ .option("--browser-pool-root <path>", "Browser pool root directory to scan")
122
+ .option("--browser-use-home <path>", "Browser-use home directory to scan for stale CDP files")
123
+ .option("--json", "Print machine-readable cleanup plan or result")
124
+ .action(async (options) => {
125
+ if (options.apply && options.dryRun)
126
+ throw new Error("Use either --apply or --dry-run, not both");
127
+ const plan = await planResourceReap(options);
128
+ const result = options.apply ? await applyResourceReapPlan(plan) : plan;
129
+ if (options.json)
130
+ printJson(options.apply ? result : { applied: false, dryRun: true, plan: result });
131
+ else
132
+ console.log(renderResourceReapText(result));
133
+ });
134
+ await program.parseAsync(argv);
135
+ }