harnery 0.41.0 → 0.42.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.
Files changed (212) hide show
  1. package/dist/commander.js +3 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +9 -1
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +113 -12
  6. package/dist/commands/browse.d.ts +164 -0
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +263 -51
  9. package/dist/commands/claude-desktop.d.ts.map +1 -1
  10. package/dist/commands/claude-desktop.js +52 -0
  11. package/dist/commands/disk.d.ts +6 -0
  12. package/dist/commands/disk.d.ts.map +1 -0
  13. package/dist/commands/disk.js +80 -0
  14. package/dist/commands/doctor.d.ts +8 -0
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +48 -1
  17. package/dist/commands/files.d.ts +16 -0
  18. package/dist/commands/files.d.ts.map +1 -1
  19. package/dist/commands/files.js +54 -2
  20. package/dist/commands/init.d.ts +2 -2
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +43 -9
  23. package/dist/commands/rm.d.ts +4 -0
  24. package/dist/commands/rm.d.ts.map +1 -0
  25. package/dist/commands/rm.js +87 -0
  26. package/dist/commands/servers.d.ts +7 -0
  27. package/dist/commands/servers.d.ts.map +1 -0
  28. package/dist/commands/servers.js +394 -0
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +82 -2
  31. package/dist/core/adapters/profiles.d.ts +9 -0
  32. package/dist/core/adapters/profiles.d.ts.map +1 -1
  33. package/dist/core/adapters/profiles.js +22 -1
  34. package/dist/core/adapters/types.d.ts +9 -0
  35. package/dist/core/adapters/types.d.ts.map +1 -1
  36. package/dist/core/agents/cli.js +13 -6
  37. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/session-context.js +2 -0
  39. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +60 -0
  42. package/dist/core/artifacts/delivery-card.js +9 -2
  43. package/dist/core/artifacts/index.d.ts +140 -4
  44. package/dist/core/artifacts/index.d.ts.map +1 -1
  45. package/dist/core/artifacts/index.js +522 -49
  46. package/dist/core/config.d.ts +68 -11
  47. package/dist/core/config.d.ts.map +1 -1
  48. package/dist/core/config.js +128 -3
  49. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  50. package/dist/core/diagnostics/bundle.js +2 -2
  51. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  52. package/dist/core/events/v3/producers/recorder.js +16 -0
  53. package/dist/core/events/v3/projection.js +9 -1
  54. package/dist/core/hooks/adapter/events.d.ts +17 -0
  55. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  56. package/dist/core/hooks/adapter/events.js +5 -0
  57. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  58. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  59. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  60. package/dist/core/hooks/adapter/wiring.js +62 -4
  61. package/dist/core/hooks/cli.js +45 -6
  62. package/dist/core/hooks/run-markers.d.ts +37 -0
  63. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  64. package/dist/core/hooks/run-markers.js +109 -0
  65. package/dist/core/servers/index.d.ts +211 -0
  66. package/dist/core/servers/index.d.ts.map +1 -0
  67. package/dist/core/servers/index.js +560 -0
  68. package/dist/core/servers/net.d.ts +58 -0
  69. package/dist/core/servers/net.d.ts.map +1 -0
  70. package/dist/core/servers/net.js +270 -0
  71. package/dist/core/servers/tunnels.d.ts +14 -0
  72. package/dist/core/servers/tunnels.d.ts.map +1 -0
  73. package/dist/core/servers/tunnels.js +33 -0
  74. package/dist/core/storage/builtins.js +1 -0
  75. package/dist/core/workflow/engine.d.ts.map +1 -1
  76. package/dist/core/workflow/engine.js +30 -1
  77. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  78. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  79. package/dist/core/workflow/sandbox-projection.js +23 -0
  80. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  81. package/dist/core/workflow/spawn-claude.js +6 -1
  82. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  83. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  84. package/dist/core/workflow/spawn-codex.js +20 -1
  85. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  86. package/dist/core/workflow/spawn-cursor.js +6 -1
  87. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  88. package/dist/core/workflow/spawn-opencode.js +4 -1
  89. package/dist/core/workflow/types.d.ts +47 -2
  90. package/dist/core/workflow/types.d.ts.map +1 -1
  91. package/dist/core/workflow/worker-access.d.ts +92 -0
  92. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  93. package/dist/core/workflow/worker-access.js +450 -0
  94. package/dist/lib/browser/batch-steps.d.ts +18 -0
  95. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  96. package/dist/lib/browser/batch-steps.js +82 -0
  97. package/dist/lib/browser/collect/collector.d.ts +99 -0
  98. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  99. package/dist/lib/browser/collect/collector.js +202 -0
  100. package/dist/lib/browser/collect/fields.d.ts +31 -0
  101. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  102. package/dist/lib/browser/collect/fields.js +75 -0
  103. package/dist/lib/browser/collect/index.d.ts +6 -0
  104. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  105. package/dist/lib/browser/collect/index.js +5 -0
  106. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  107. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  108. package/dist/lib/browser/collect/page-driver.js +306 -0
  109. package/dist/lib/browser/collect/presets.d.ts +52 -0
  110. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  111. package/dist/lib/browser/collect/presets.js +176 -0
  112. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  113. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  114. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  115. package/dist/lib/browser/index.d.ts +1 -1
  116. package/dist/lib/browser/index.d.ts.map +1 -1
  117. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  118. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  119. package/dist/lib/browser/page-review-judge.js +13 -0
  120. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  121. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  122. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  123. package/dist/lib/browser/qa-run.js +1 -0
  124. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  125. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  126. package/dist/lib/browser/windows-chrome.js +179 -0
  127. package/dist/lib/claude-desktop-share.d.ts +136 -0
  128. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  129. package/dist/lib/claude-desktop-share.js +394 -0
  130. package/dist/lib/disk-usage.d.ts +50 -0
  131. package/dist/lib/disk-usage.d.ts.map +1 -0
  132. package/dist/lib/disk-usage.js +303 -0
  133. package/dist/lib/docs-lint.d.ts +6 -0
  134. package/dist/lib/docs-lint.d.ts.map +1 -1
  135. package/dist/lib/docs-lint.js +8 -3
  136. package/dist/lib/guarded-remove.d.ts +34 -0
  137. package/dist/lib/guarded-remove.d.ts.map +1 -0
  138. package/dist/lib/guarded-remove.js +310 -0
  139. package/dist/lib/instructions/templates.js +8 -8
  140. package/dist/lib/tunnel/error-page.d.ts +1 -1
  141. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  142. package/dist/lib/tunnel/error-page.js +19 -8
  143. package/dist/lib/tunnel/gate.js +20 -1
  144. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  145. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  146. package/dist/lib/tunnel/path-scope.js +62 -0
  147. package/dist/lib/tunnel/state.d.ts +5 -0
  148. package/dist/lib/tunnel/state.d.ts.map +1 -1
  149. package/dist/lib/tunnel/state.js +17 -3
  150. package/package.json +6 -1
  151. package/schemas/config.schema.json +91 -0
  152. package/src/commander.ts +19 -0
  153. package/src/commands/agents.ts +14 -0
  154. package/src/commands/artifacts.ts +171 -19
  155. package/src/commands/browse.ts +400 -57
  156. package/src/commands/claude-desktop.ts +61 -0
  157. package/src/commands/disk.ts +114 -0
  158. package/src/commands/doctor.ts +52 -1
  159. package/src/commands/files.ts +93 -2
  160. package/src/commands/init.ts +55 -5
  161. package/src/commands/rm.ts +115 -0
  162. package/src/commands/servers.ts +506 -0
  163. package/src/commands/tunnel.ts +102 -2
  164. package/src/core/adapters/profiles.ts +22 -1
  165. package/src/core/adapters/types.ts +10 -0
  166. package/src/core/agents/cli.ts +14 -6
  167. package/src/core/agents/render/session-context.ts +1 -0
  168. package/src/core/agents/state/live-coordination-view.ts +73 -0
  169. package/src/core/artifacts/delivery-card.ts +10 -2
  170. package/src/core/artifacts/index.ts +697 -44
  171. package/src/core/config.ts +183 -4
  172. package/src/core/diagnostics/bundle.ts +2 -1
  173. package/src/core/events/v3/producers/recorder.ts +16 -0
  174. package/src/core/events/v3/projection.ts +11 -1
  175. package/src/core/hooks/adapter/events.ts +19 -0
  176. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  177. package/src/core/hooks/adapter/wiring.ts +116 -8
  178. package/src/core/hooks/cli.ts +47 -6
  179. package/src/core/hooks/run-markers.ts +132 -0
  180. package/src/core/servers/index.ts +712 -0
  181. package/src/core/servers/net.ts +283 -0
  182. package/src/core/servers/tunnels.ts +39 -0
  183. package/src/core/storage/builtins.ts +1 -0
  184. package/src/core/workflow/engine.ts +38 -1
  185. package/src/core/workflow/sandbox-projection.ts +38 -2
  186. package/src/core/workflow/spawn-claude.ts +5 -1
  187. package/src/core/workflow/spawn-codex.ts +18 -1
  188. package/src/core/workflow/spawn-cursor.ts +5 -1
  189. package/src/core/workflow/spawn-opencode.ts +4 -1
  190. package/src/core/workflow/types.ts +62 -2
  191. package/src/core/workflow/worker-access.ts +525 -0
  192. package/src/lib/browser/batch-steps.ts +79 -0
  193. package/src/lib/browser/collect/collector.ts +306 -0
  194. package/src/lib/browser/collect/fields.ts +87 -0
  195. package/src/lib/browser/collect/index.ts +47 -0
  196. package/src/lib/browser/collect/page-driver.ts +327 -0
  197. package/src/lib/browser/collect/presets.ts +237 -0
  198. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  199. package/src/lib/browser/index.ts +1 -0
  200. package/src/lib/browser/page-review-judge.ts +16 -0
  201. package/src/lib/browser/qa-run-contracts.ts +11 -0
  202. package/src/lib/browser/qa-run.ts +1 -0
  203. package/src/lib/browser/windows-chrome.ts +227 -0
  204. package/src/lib/claude-desktop-share.ts +511 -0
  205. package/src/lib/disk-usage.ts +347 -0
  206. package/src/lib/docs-lint.ts +8 -3
  207. package/src/lib/guarded-remove.ts +376 -0
  208. package/src/lib/instructions/templates.ts +8 -8
  209. package/src/lib/tunnel/error-page.ts +25 -9
  210. package/src/lib/tunnel/gate.ts +22 -1
  211. package/src/lib/tunnel/path-scope.ts +73 -0
  212. package/src/lib/tunnel/state.ts +20 -3
@@ -5,8 +5,11 @@ import { dirname, isAbsolute, resolve } from "node:path";
5
5
  import { chromium } from "playwright";
6
6
  import { PNG } from "pngjs";
7
7
  import { resolveBinName } from "../core/config.js";
8
+ import { splitBatchSteps } from "../lib/browser/batch-steps.js";
8
9
  import { CAPTURE_FIDELITY_MISMATCH_THRESHOLD, compareBand, cropNativePng, decideFidelity, pngDimensions, } from "../lib/browser/capture-fidelity.js";
10
+ import { createPageDriver, createRng, DEFAULT_COLLECT_LIMITS, PACING_LIMITS, parseProbability, parseRange, presetNames, randomSeed, resolveCollectSpec, runCollect, scrollPacingFromEnv, } from "../lib/browser/collect/index.js";
9
11
  import { Browser, browserProxyFromEnv, browserProxyGateFromEnv, captureDevOverlay, chromeMajorFromExecutable, DEFAULT_CRITIQUE_RUBRIC, extractObservedIp, installedChromeChannel, installedChromePath, parseAssertSpec, resolveUserAgent, runCritique, tilesFromFullPage, WEBRTC_PROXY_ONLY_ARG, writeNetscapeCookieFile, wslHeadedLaunchArgs, } from "../lib/browser/index.js";
12
+ import { isWSL } from "../lib/browser/launch-args.js";
10
13
  import { allocateTileBudget, validatePageReviewAllocation, } from "../lib/browser/page-review-budget.js";
11
14
  import { PAGE_REVIEW_CAPTURE_PLAN_SCHEMA, } from "../lib/browser/page-review-contracts.js";
12
15
  import { findPackTile, gateHitsFromEnvelope, readPackContext, tileId, writePackContext, writePackExpandedTile, } from "../lib/browser/page-review-pack.js";
@@ -19,7 +22,8 @@ import { startBrowserSessionServer, } from "../lib/browser/session-control.js";
19
22
  import { captureThumbnailFingerprint, registerCaptureThumbnail, } from "../lib/browser/thumbnail-association.js";
20
23
  import { reviewCandidateRects } from "../lib/browser/tiling.js";
21
24
  import { diffAgainstBaseline, saveBaseline, } from "../lib/browser/visual-diff.js";
22
- import { applyExtraCookies, CookieJar } from "../lib/cookies/index.js";
25
+ import { harvestDomain, harvestWindowsChromeCookies, stopWindowsChromeProfile, windowsChromePath, windowsLocalAppData, windowsPlainProfile, wslPathToWindows, } from "../lib/browser/windows-chrome.js";
26
+ import { applyExtraCookies, CookieJar, mergeCookies } from "../lib/cookies/index.js";
23
27
  import { commandPaceGate } from "../lib/pace/index.js";
24
28
  /**
25
29
  * `harn browse <url>`: Playwright-backed page navigation with shared
@@ -73,21 +77,46 @@ export function registerBrowseCommand(program, emitParam, context, binName = res
73
77
  .option("--capture-evaluate <js>", "In trio mode, evaluate JS inside the exact screenshot viewport immediately before capture; result is written as captureEval")
74
78
  .option("--batch <steps>", "Run multiple steps in one session, semicolon-separated. Each step is one of: " +
75
79
  "`click <selector>`, `fill <selector=>value>`, `press <key>`, `wait <selector|ms>`, `eval <js>`, `reload`. " +
76
- 'Example: `--batch "click button; wait 1500; reload; wait 3000"`. `reload` preserves sessionStorage + cookies, which is how to repro sessionStorage-restored UI state.')
80
+ 'Example: `--batch "click button; wait 1500; reload; wait 3000"`. `reload` preserves sessionStorage + cookies, which is how to repro sessionStorage-restored UI state. ' +
81
+ "An `eval` step keeps semicolons inside JS quotes and (), [], {}; `\\;` is always a literal semicolon.")
82
+ .option("--collect [preset]", "Scroll an infinite feed like a reader and collect every item that passes through the DOM, " +
83
+ `de-duplicated, as JSON. Presets: ${presetNames().join(", ")} (default generic). ` +
84
+ "Skips the trio, checks and --evaluate; runs after --wait-for and --batch.")
85
+ .option("--collect-item <css>", "Item selector (overrides the preset's).")
86
+ .option("--collect-key <extractor>", "Dedupe key extractor, <selector>[@attr] inside the item. Default: the preset key, else the " +
87
+ "first permalink-like link, else a hash of the item text.")
88
+ .option("--collect-key-pattern <regex>", "Regex a link must match to count as a permalink key.")
89
+ .option("--collect-field <name=extractor>", "Add or override a field: name=<selector>[@attr]. name[]= collects every match. Empty selector " +
90
+ "or :scope is the item itself; no @attr reads visible text (repeatable).", (value, previous = []) => [...previous, value], [])
91
+ .option("--collect-expand [selector]", "Click 'show more' buttons inside visible items before harvesting (preset selector by default). " +
92
+ "At most 3 per step and 50 per run; links and action buttons are never clicked.")
93
+ .option("--collect-max-items <n>", "Stop after this many unique items.")
94
+ .option("--collect-max-scrolls <n>", "Stop after this many scroll steps (default 60).", "60")
95
+ .option("--collect-max-seconds <n>", "Stop after this much wall time (default 300).", "300")
96
+ .option("--collect-idle <n>", "Stop after this many steps with no new items and no page growth (default 5).", "5")
97
+ .option("--collect-step <min-max>", "Scroll distance per step as a viewport fraction (default 0.6-1.1; env HARNERY_COLLECT_STEP).")
98
+ .option("--collect-pause <min-max>", "Pause after each step in ms (default 1200-3500; env HARNERY_COLLECT_PAUSE_MS).")
99
+ .option("--collect-read-chance <p>", "Chance of a longer reading pause per step (default 0.12).")
100
+ .option("--collect-read-pause <min-max>", "Reading pause in ms (default 4000-9000).")
101
+ .option("--collect-back-chance <p>", "Chance a step starts with a small scroll back up (default 0.08).")
102
+ .option("--collect-seed <n>", "Seed for the scroll schedule; the envelope reports the seed used.")
103
+ .option("--collect-format <format>", "json (envelope, default) or jsonl (one item per line).", "json")
104
+ .option("--collect-out <path>", "Write the collection to this file instead of stdout.")
77
105
  .option("--network-har <path>", "Record network traffic to a HAR file (finalized on close)")
78
106
  .option("--viewport <preset|WxH>", "Viewport: mobile (390x844), tablet (820x1180), desktop (1280x800), hd (1920x1080), or explicit '1920x1080'. Default: real window in headed mode, desktop in headless mode")
79
107
  .option("--color-scheme <scheme>", "Emulate prefers-color-scheme for the whole session: light | dark. " +
80
108
  "Renders theme-aware pages in that scheme without page-specific toggles. " +
81
109
  "Absent = browser default (unchanged behavior).")
82
- .option("--login", "Headed mode for one-time auth flow (cookies persist in profile)")
83
- .option("--plain", "With --login: open the URL in the installed Google Chrome with nothing attached (no DevTools " +
84
- "protocol, no Playwright) on the same persistent profile. Use it for sign-in flows whose " +
85
- "verification step detects automation itself (X does). Sign in, then close the window or " +
86
- "create the --login-close-file; later browse and browse-session runs reuse the profile's cookies. " +
87
- "Not compatible with --control-file, because nothing is attached to drive.")
110
+ .option("--login", "Open the URL in the installed Google Chrome with nothing attached (no DevTools protocol, no " +
111
+ "Playwright) so a person can sign in; under WSL this is the Windows host's Chrome. Sign in, then " +
112
+ "close the window or create the --login-close-file; later browse and browse-session runs reuse " +
113
+ "the cookies. Sites cannot tell this window from the person's own browser.")
114
+ .option("--attached", "With --login: open an automation-controlled Chrome that this process and --control-file can drive. " +
115
+ "Sites can detect it and may lock the account, so use it only for agent-driven headed sessions on " +
116
+ "sites you own. Never use it to sign in to a third-party account.")
88
117
  .option("--login-close-file <path>", "With --login, wait for this file instead of terminal Enter, then remove it and close cleanly")
89
- .option("--control-file <path>", "With --login, publish an owner-only descriptor for repeated browse-session control")
90
- .option("--headed", "Headed mode for one-off (no auth-flow framing)")
118
+ .option("--control-file <path>", "With --login --attached, publish an owner-only descriptor for repeated browse-session control")
119
+ .option("--headed", "Automation-controlled headed window for one-off visual checks. Not for signing in; use --login.")
91
120
  .option("--browser-channel <name>", "Browser to launch: chrome | chrome-beta | msedge | chromium. Headed launches (--login, --headed) " +
92
121
  "default to the installed Google Chrome with the automation signals removed, because the bundled " +
93
122
  '"Chrome for Testing" build advertises itself and sign-in flows refuse clicks; chromium forces the ' +
@@ -314,7 +343,8 @@ async function runBrowse(url, opts, context) {
314
343
  : new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse" });
315
344
  if (jar)
316
345
  applyExtraCookies(url, jar, context?.extraCookies);
317
- const headed = opts.login || opts.headed;
346
+ const loginMode = resolveLoginMode(opts);
347
+ const headed = loginMode === "attached" || opts.headed;
318
348
  const viewport = parseViewport(opts.viewport ?? "desktop");
319
349
  const deviceScaleFactor = parseDeviceScaleFactor(opts.deviceScaleFactor);
320
350
  if (opts.reviewPackAllocation && !opts.reviewPack) {
@@ -340,23 +370,13 @@ async function runBrowse(url, opts, context) {
340
370
  }
341
371
  const proxy = opts.proxyFromEnv ? browserProxyFromEnv() : undefined;
342
372
  const proxyGate = opts.proxyFromEnv ? browserProxyGateFromEnv() : null;
343
- if (opts.loginCloseFile && !opts.login) {
344
- throw new Error("--login-close-file requires --login.");
345
- }
346
- if (opts.controlFile && !opts.login) {
347
- throw new Error("--control-file requires --login.");
348
- }
349
373
  const loginCloseFile = opts.loginCloseFile ? resolve(opts.loginCloseFile) : null;
350
374
  if (loginCloseFile && existsSync(loginCloseFile)) {
351
375
  throw new Error(`Login close signal already exists at ${loginCloseFile}. Remove the stale file before launching.`);
352
376
  }
377
+ const collectPlan = opts.collect ? buildCollectPlan(opts) : null;
353
378
  const pace = commandPaceGate(opts.pace !== false, (message) => emit.log(message, "info"));
354
- if (opts.plain) {
355
- if (!opts.login)
356
- throw new Error("--plain requires --login.");
357
- if (opts.controlFile) {
358
- throw new Error("--plain cannot take --control-file: nothing is attached to drive the window.");
359
- }
379
+ if (loginMode === "plain") {
360
380
  const plainExecutable = installedChromePath();
361
381
  const plainUa = resolveUserAgent({
362
382
  requested: opts.userAgent,
@@ -364,6 +384,10 @@ async function runBrowse(url, opts, context) {
364
384
  chromeMajor: plainExecutable ? chromeMajorFromExecutable(plainExecutable) : undefined,
365
385
  });
366
386
  await pace?.before(url);
387
+ if (useWindowsPlainLogin()) {
388
+ await runWindowsPlainLogin(url, opts.store ?? DEFAULT_STORE, loginCloseFile, emit);
389
+ return;
390
+ }
367
391
  await runPlainLogin(url, opts.profile ?? DEFAULT_PROFILE, loginCloseFile, plainUa, emit);
368
392
  return;
369
393
  }
@@ -434,6 +458,10 @@ async function runBrowse(url, opts, context) {
434
458
  if (opts.batch) {
435
459
  batchResult = await runBatch(browser, opts.batch, Number.parseInt(opts.timeout, 10));
436
460
  }
461
+ if (collectPlan) {
462
+ await runCollectMode(browser, collectPlan);
463
+ return;
464
+ }
437
465
  let evalResult;
438
466
  if (opts.evaluate) {
439
467
  evalResult = await browser.evaluate(opts.evaluate);
@@ -824,11 +852,11 @@ async function runBrowse(url, opts, context) {
824
852
  const lifecycleAbort = new AbortController();
825
853
  const closeWaits = [];
826
854
  if (loginCloseFile) {
827
- emit.log(`[--login] Headed Chromium is open. Drive the visible window now. Create ${loginCloseFile} to close cleanly.`, "info");
855
+ emit.log(`[--login --attached] Automation-controlled Chrome is open. Drive the visible window now. Create ${loginCloseFile} to close cleanly.`, "info");
828
856
  closeWaits.push(waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal));
829
857
  }
830
858
  else {
831
- emit.log("[--login] Headed Chromium is open. Walk through your auth flow now. Press Enter here to close + persist cookies into the profile.", "info");
859
+ emit.log("[--login --attached] Automation-controlled Chrome is open. Drive the visible window now. Press Enter here to close + persist cookies into the profile.", "info");
832
860
  closeWaits.push(waitForTerminalEnter(lifecycleAbort.signal));
833
861
  }
834
862
  if (controlServer) {
@@ -964,6 +992,137 @@ async function runBrowse(url, opts, context) {
964
992
  process.exitCode = receivedSignal === "SIGINT" ? 130 : 143;
965
993
  }
966
994
  }
995
+ function parsePositiveInt(raw, flag) {
996
+ if (!/^\d+$/.test(raw.trim()) || Number.parseInt(raw, 10) < 1) {
997
+ throw new Error(`${flag} must be a positive integer (got "${raw}").`);
998
+ }
999
+ return Number.parseInt(raw, 10);
1000
+ }
1001
+ function buildCollectPlan(opts) {
1002
+ if (opts.login)
1003
+ throw new Error("--collect cannot run with --login.");
1004
+ if (opts.snapshot || opts.html || opts.json) {
1005
+ throw new Error("--collect prints its own JSON; drop --snapshot, --html and --json.");
1006
+ }
1007
+ const presetName = typeof opts.collect === "string" ? opts.collect : "generic";
1008
+ const spec = resolveCollectSpec(presetName, {
1009
+ item: opts.collectItem,
1010
+ key: opts.collectKey,
1011
+ keyPattern: opts.collectKeyPattern,
1012
+ fields: opts.collectField,
1013
+ expand: opts.collectExpand,
1014
+ });
1015
+ const limits = {
1016
+ ...DEFAULT_COLLECT_LIMITS,
1017
+ maxItems: opts.collectMaxItems === undefined
1018
+ ? null
1019
+ : parsePositiveInt(opts.collectMaxItems, "--collect-max-items"),
1020
+ maxScrolls: parsePositiveInt(opts.collectMaxScrolls ?? "60", "--collect-max-scrolls"),
1021
+ maxMs: parsePositiveInt(opts.collectMaxSeconds ?? "300", "--collect-max-seconds") * 1_000,
1022
+ idleSteps: parsePositiveInt(opts.collectIdle ?? "5", "--collect-idle"),
1023
+ expandPerStep: spec.expand ? 3 : 0,
1024
+ };
1025
+ const pacing = scrollPacingFromEnv();
1026
+ const msBounds = { min: 0, max: PACING_LIMITS.maxPauseMs };
1027
+ if (opts.collectStep) {
1028
+ pacing.step = parseRange(opts.collectStep, "--collect-step", {
1029
+ min: 0.05,
1030
+ max: PACING_LIMITS.maxStepFraction,
1031
+ });
1032
+ }
1033
+ if (opts.collectPause)
1034
+ pacing.pauseMs = parseRange(opts.collectPause, "--collect-pause", msBounds);
1035
+ if (opts.collectReadPause) {
1036
+ pacing.readPauseMs = parseRange(opts.collectReadPause, "--collect-read-pause", msBounds);
1037
+ }
1038
+ if (opts.collectReadChance !== undefined) {
1039
+ pacing.readChance = parseProbability(opts.collectReadChance, "--collect-read-chance");
1040
+ }
1041
+ if (opts.collectBackChance !== undefined) {
1042
+ pacing.backChance = parseProbability(opts.collectBackChance, "--collect-back-chance");
1043
+ }
1044
+ let seed = randomSeed();
1045
+ if (opts.collectSeed !== undefined) {
1046
+ if (!/^\d+$/.test(opts.collectSeed.trim())) {
1047
+ throw new Error(`--collect-seed must be a non-negative integer (got "${opts.collectSeed}").`);
1048
+ }
1049
+ seed = Number(BigInt(opts.collectSeed.trim()) % 4294967296n);
1050
+ }
1051
+ const format = opts.collectFormat ?? "json";
1052
+ if (format !== "json" && format !== "jsonl") {
1053
+ throw new Error(`--collect-format must be json or jsonl (got "${format}").`);
1054
+ }
1055
+ return {
1056
+ spec,
1057
+ limits,
1058
+ pacing,
1059
+ seed,
1060
+ format,
1061
+ out: opts.collectOut ? resolve(opts.collectOut) : null,
1062
+ };
1063
+ }
1064
+ async function runCollectMode(browser, plan) {
1065
+ const page = browser.currentPage;
1066
+ const rng = createRng(plan.seed);
1067
+ let interrupted = false;
1068
+ const onSignal = () => {
1069
+ interrupted = true;
1070
+ };
1071
+ process.on("SIGINT", onSignal);
1072
+ process.on("SIGTERM", onSignal);
1073
+ let result;
1074
+ try {
1075
+ emit.log(`collect: preset ${plan.spec.preset}, item ${plan.spec.item}, seed ${plan.seed}`, "info");
1076
+ const driver = createPageDriver(page, plan.spec, {
1077
+ rng,
1078
+ log: (message) => emit.log(message, "warn"),
1079
+ });
1080
+ result = await runCollect(driver, {
1081
+ limits: plan.limits,
1082
+ pacing: plan.pacing,
1083
+ rng,
1084
+ shouldStop: () => interrupted,
1085
+ onStep: ({ step, items, added }) => emit.log(`collect: step ${step}, ${items} items (+${added})`, "debug"),
1086
+ });
1087
+ }
1088
+ finally {
1089
+ process.removeListener("SIGINT", onSignal);
1090
+ process.removeListener("SIGTERM", onSignal);
1091
+ }
1092
+ const { items, stats } = result;
1093
+ emit.log(`collect: ${items.length} items in ${stats.steps} steps, stopped on ${stats.stopReason}` +
1094
+ (stats.stopDetail ? ` (${stats.stopDetail})` : ""), "info");
1095
+ const envelope = {
1096
+ url: page.url(),
1097
+ preset: plan.spec.preset,
1098
+ itemSelector: plan.spec.item,
1099
+ seed: plan.seed,
1100
+ stopReason: stats.stopReason,
1101
+ stats,
1102
+ items,
1103
+ };
1104
+ if (plan.format === "jsonl") {
1105
+ const body = items.map((item) => JSON.stringify(item)).join("\n");
1106
+ if (plan.out) {
1107
+ mkdirSync(dirname(plan.out), { recursive: true });
1108
+ writeFileSync(plan.out, body ? `${body}\n` : "");
1109
+ emit.file(plan.out, { items: items.length, stopReason: stats.stopReason, stats });
1110
+ }
1111
+ else if (body) {
1112
+ process.stdout.write(`${body}\n`);
1113
+ }
1114
+ }
1115
+ else if (plan.out) {
1116
+ mkdirSync(dirname(plan.out), { recursive: true });
1117
+ writeFileSync(plan.out, `${JSON.stringify(envelope, null, 2)}\n`);
1118
+ emit.file(plan.out, { items: items.length, stopReason: stats.stopReason, stats });
1119
+ }
1120
+ else {
1121
+ emit.data(envelope);
1122
+ }
1123
+ if (interrupted)
1124
+ process.exitCode = 130;
1125
+ }
967
1126
  async function runBatch(browser, batch, defaultTimeoutMs) {
968
1127
  const result = { clipboardReads: [] };
969
1128
  const steps = splitBatchSteps(batch);
@@ -1034,27 +1193,6 @@ async function runBatch(browser, batch, defaultTimeoutMs) {
1034
1193
  }
1035
1194
  return result;
1036
1195
  }
1037
- function splitBatchSteps(input) {
1038
- const steps = [];
1039
- let buf = "";
1040
- for (let i = 0; i < input.length; i++) {
1041
- const ch = input[i];
1042
- if (ch === "\\" && input[i + 1] === ";") {
1043
- buf += ";";
1044
- i++;
1045
- continue;
1046
- }
1047
- if (ch === ";") {
1048
- steps.push(buf);
1049
- buf = "";
1050
- continue;
1051
- }
1052
- buf += ch;
1053
- }
1054
- if (buf.trim())
1055
- steps.push(buf);
1056
- return steps;
1057
- }
1058
1196
  // ---------------------------------------------------------------------------
1059
1197
  // Print mode (--snapshot / --html / --json): stdout, no file writes
1060
1198
  // ---------------------------------------------------------------------------
@@ -2155,7 +2293,26 @@ async function verifyBrowserProxyGate(browser, gate) {
2155
2293
  emit.log(`browser proxy gate: ${observedIp} · expected IP confirmed`, "info");
2156
2294
  }
2157
2295
  /**
2158
- * `--login --plain`: spawn the installed Google Chrome directly on the shared
2296
+ * How a `browse` run handles sign-in. `--login` opens the installed Google
2297
+ * Chrome with nothing attached, because sites can detect an
2298
+ * automation-controlled browser and disable the account that signed in with
2299
+ * it. The attached window is reachable only through an explicit `--attached`.
2300
+ */
2301
+ export function resolveLoginMode(opts) {
2302
+ if (opts.attached && !opts.login)
2303
+ throw new Error("--attached requires --login.");
2304
+ if (opts.loginCloseFile && !opts.login)
2305
+ throw new Error("--login-close-file requires --login.");
2306
+ if (opts.controlFile && !(opts.login && opts.attached)) {
2307
+ throw new Error("--control-file requires --login --attached. A sign-in to a third-party account uses plain " +
2308
+ "--login, which nothing can drive.");
2309
+ }
2310
+ if (!opts.login)
2311
+ return "none";
2312
+ return opts.attached ? "attached" : "plain";
2313
+ }
2314
+ /**
2315
+ * `--login`: spawn the installed Google Chrome directly on the shared
2159
2316
  * persistent profile, with no DevTools protocol attached. Playwright enables
2160
2317
  * its runtime hooks in every frame it drives, and sign-in verification vendors
2161
2318
  * (X's is the reported case) detect that and fail the flow with no explanation.
@@ -2166,7 +2323,8 @@ async function verifyBrowserProxyGate(browser, gate) {
2166
2323
  async function runPlainLogin(url, profileDir, loginCloseFile, userAgent, emit) {
2167
2324
  const executable = installedChromePath();
2168
2325
  if (!executable) {
2169
- throw new Error("--plain needs an installed Google Chrome at a standard path; none was found on this machine.");
2326
+ throw new Error("--login needs an installed Google Chrome at a standard path; none was found on this machine. " +
2327
+ "Install Google Chrome; do not fall back to --attached for a third-party sign-in.");
2170
2328
  }
2171
2329
  mkdirSync(profileDir, { recursive: true });
2172
2330
  const child = spawn(executable, [
@@ -2183,11 +2341,11 @@ async function runPlainLogin(url, profileDir, loginCloseFile, userAgent, emit) {
2183
2341
  const lifecycleAbort = new AbortController();
2184
2342
  const waits = [exited];
2185
2343
  if (loginCloseFile) {
2186
- emit.log(`[--login --plain] Plain Google Chrome is open with nothing attached. Sign in, then close the window or create ${loginCloseFile}.`, "info");
2344
+ emit.log(`[--login] Plain Google Chrome is open with nothing attached. Sign in, then close the window or create ${loginCloseFile}.`, "info");
2187
2345
  waits.push(waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal));
2188
2346
  }
2189
2347
  else {
2190
- emit.log("[--login --plain] Plain Google Chrome is open with nothing attached. Sign in, then close the window or press Enter here.", "info");
2348
+ emit.log("[--login] Plain Google Chrome is open with nothing attached. Sign in, then close the window or press Enter here.", "info");
2191
2349
  waits.push(waitForTerminalEnter(lifecycleAbort.signal));
2192
2350
  }
2193
2351
  await Promise.race(waits);
@@ -2196,7 +2354,61 @@ async function runPlainLogin(url, profileDir, loginCloseFile, userAgent, emit) {
2196
2354
  child.kill("SIGTERM");
2197
2355
  await Promise.race([exited, new Promise((r) => setTimeout(r, 5000))]);
2198
2356
  }
2199
- emit.log(`[--login --plain] Chrome closed; cookies persisted in ${profileDir}.`, "info");
2357
+ emit.log(`[--login] Chrome closed; cookies persisted in ${profileDir}.`, "info");
2358
+ }
2359
+ /**
2360
+ * Under WSL, `--login` opens the Windows host's Google Chrome when one
2361
+ * is installed. Some sign-in flows reject every browser running inside WSL,
2362
+ * even a plain Linux Chrome, while the Windows desktop Chrome signs in
2363
+ * normally. `HARNERY_BROWSER_WSL_PLAIN=linux` keeps the Linux Chrome.
2364
+ */
2365
+ function useWindowsPlainLogin() {
2366
+ if (!isWSL())
2367
+ return false;
2368
+ if ((process.env.HARNERY_BROWSER_WSL_PLAIN ?? "").toLowerCase() === "linux")
2369
+ return false;
2370
+ return windowsChromePath() !== undefined;
2371
+ }
2372
+ /**
2373
+ * The WSL form of `--login`: the person signs in through the Windows
2374
+ * Chrome on a dedicated Windows profile with nothing attached. When the window
2375
+ * closes, the profile's cookies for the sign-in site are read and merged into
2376
+ * the shared cookie store, which later `browse` runs attach.
2377
+ */
2378
+ async function runWindowsPlainLogin(url, storePath, loginCloseFile, emit) {
2379
+ const chromeLinux = windowsChromePath();
2380
+ const localAppData = windowsLocalAppData();
2381
+ if (!chromeLinux || !localAppData) {
2382
+ throw new Error("--login under WSL needs the Windows Google Chrome and %LOCALAPPDATA%; neither was readable.");
2383
+ }
2384
+ const chromeWindows = wslPathToWindows(chromeLinux);
2385
+ const profileWindows = windowsPlainProfile(localAppData);
2386
+ const child = spawn(chromeLinux, [`--user-data-dir=${profileWindows}`, "--no-first-run", "--no-default-browser-check", url], { stdio: "ignore" });
2387
+ const exited = new Promise((resolveExit) => {
2388
+ child.once("exit", () => resolveExit());
2389
+ child.once("error", () => resolveExit());
2390
+ });
2391
+ const lifecycleAbort = new AbortController();
2392
+ const waits = [exited];
2393
+ const how = loginCloseFile ? `create ${loginCloseFile}` : "press Enter here";
2394
+ emit.log(`[--login] Windows Google Chrome is open with nothing attached (profile ${profileWindows}). Sign in, then close the window or ${how}.`, "info");
2395
+ waits.push(loginCloseFile
2396
+ ? waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal)
2397
+ : waitForTerminalEnter(lifecycleAbort.signal));
2398
+ await Promise.race(waits);
2399
+ lifecycleAbort.abort();
2400
+ stopWindowsChromeProfile(profileWindows);
2401
+ await Promise.race([exited, new Promise((r) => setTimeout(r, 5000))]);
2402
+ const domain = harvestDomain(url);
2403
+ const cookies = harvestWindowsChromeCookies({ chromeWindows, profileWindows, domain });
2404
+ if (cookies.length === 0) {
2405
+ emit.log(`[--login] No ${domain} cookies were found in the Windows profile; the sign-in may not have completed.`, "warn");
2406
+ return;
2407
+ }
2408
+ const jar = new CookieJar({ path: storePath, source: "harn-browse" });
2409
+ jar.save(mergeCookies(jar.load(), cookies));
2410
+ const names = [...new Set(cookies.map((cookie) => cookie.name))].sort().join(", ");
2411
+ emit.log(`[--login] Copied ${cookies.length} ${domain} cookies into ${storePath}: ${names}.`, "info");
2200
2412
  }
2201
2413
  async function waitForLoginCloseFile(path, signal) {
2202
2414
  while (!existsSync(path) && !signal?.aborted) {
@@ -1 +1 @@
1
- {"version":3,"file":"claude-desktop.d.ts","sourceRoot":"","sources":["../../src/commands/claude-desktop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAU1E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,QAAQ,CAAC,EAAE,qBAAqB,GAC/B,IAAI,CA8JN"}
1
+ {"version":3,"file":"claude-desktop.d.ts","sourceRoot":"","sources":["../../src/commands/claude-desktop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAiB1E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,WAAW,EACjB,QAAQ,CAAC,EAAE,qBAAqB,GAC/B,IAAI,CAoNN"}
@@ -1,4 +1,6 @@
1
1
  import { applyMirror, findDesktopDataDirs, listAccounts, planMirror, readCliAccount, } from "../lib/claude-desktop.js";
2
+ import { applyShare, defaultShareDir, planShare, shareSummary, } from "../lib/claude-desktop-share.js";
3
+ import { resolveMachineLabel } from "../lib/machine.js";
2
4
  /**
3
5
  * `claude-desktop`: make Claude desktop-app sessions survive account
4
6
  * switches.
@@ -141,6 +143,56 @@ export function registerClaudeDesktopCommand(program, emit, _context) {
141
143
  });
142
144
  }
143
145
  });
146
+ cmd
147
+ .command("share")
148
+ .description("Share sessions with your other computers through a synced folder (iCloud Drive by default): " +
149
+ "publish this machine's recent sessions and list + resume every other machine's here. " +
150
+ "Dry-run by default; --yes applies. Restart the desktop app afterward to see new entries.")
151
+ .option("--dir <path>", "Shared folder (default: iCloud Drive/Harnery/claude-sessions)")
152
+ .option("--days <n>", "Only sessions active in the last N days", "14")
153
+ .option("--data-dir <path>", "Explicit desktop-app data directory")
154
+ .option("--yes", "Apply the plan (default is dry-run)")
155
+ .action((opts) => {
156
+ const shareDir = opts.dir ?? process.env.HARNERY_SESSION_SHARE_DIR ?? defaultShareDir();
157
+ if (!shareDir) {
158
+ emit.error({
159
+ code: "no_share_dir",
160
+ message: "no shared folder: pass --dir <path> or set HARNERY_SESSION_SHARE_DIR",
161
+ });
162
+ process.exit(2);
163
+ }
164
+ const cli = readCliAccount();
165
+ if (!cli) {
166
+ emit.error({
167
+ code: "no_account",
168
+ message: "cannot read the signed-in account from ~/.claude.json",
169
+ });
170
+ process.exit(2);
171
+ }
172
+ const [dataDir] = requireDataDirs(emit, opts.dataDir);
173
+ const shareOpts = {
174
+ shareDir,
175
+ machine: resolveMachineLabel(),
176
+ days: Number(opts.days) || 14,
177
+ dataDir: dataDir,
178
+ targetAccountUuid: cli.accountUuid,
179
+ };
180
+ const plan = planShare(shareOpts);
181
+ const summary = { share_dir: shareDir, machine: shareOpts.machine, ...shareSummary(plan) };
182
+ if (!opts.yes) {
183
+ emit.data({ dry_run: true, ...summary, hint: "re-run with --yes to apply" });
184
+ return;
185
+ }
186
+ const result = applyShare(plan, shareOpts);
187
+ emit.data({
188
+ dry_run: false,
189
+ ...summary,
190
+ ...result,
191
+ hint: result.imported > 0
192
+ ? "quit the Claude desktop app fully (Cmd+Q) and reopen it to see the new sessions"
193
+ : "sidebar already current",
194
+ });
195
+ });
144
196
  }
145
197
  function collect(value, previous) {
146
198
  return previous.concat([value]);
@@ -0,0 +1,6 @@
1
+ import { type Command } from "commander";
2
+ import type { EmitContext } from "../commander.js";
3
+ import { type DiskReport } from "../lib/disk-usage.js";
4
+ export declare function registerDiskCommand(program: Command, emit: EmitContext): void;
5
+ export declare function renderDiskUsage(report: DiskReport): string;
6
+ //# sourceMappingURL=disk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disk.d.ts","sourceRoot":"","sources":["../../src/commands/disk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAwB,MAAM,WAAW,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAGL,KAAK,UAAU,EAGhB,MAAM,sBAAsB,CAAC;AAQ9B,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA+C7E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAgD1D"}
@@ -0,0 +1,80 @@
1
+ import { InvalidArgumentError } from "commander";
2
+ import { diskUsage, parseDiskSize, } from "../lib/disk-usage.js";
3
+ function positive(value) {
4
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < 1)
5
+ throw new InvalidArgumentError("requires a positive integer");
6
+ return Number(value);
7
+ }
8
+ export function registerDiskCommand(program, emit) {
9
+ program
10
+ .command("disk [path]")
11
+ .description("Measure checkout disk usage, including nested repositories and ignored files")
12
+ .option("--git <states>", "Filter: tracked, untracked, ignored, git-metadata, non-repository, unknown (comma-separated)")
13
+ .option("--type <types>", "File extensions or video, audio, image, media, archive (comma-separated)")
14
+ .option("--exclude <paths>", "Skip directory names at any depth or root-relative paths; repeatable, comma-separated", (value, previous) => [...previous, value], [])
15
+ .option("--group <by>", "Group by directory, repository, extension, git, or files", "directory")
16
+ .option("--depth <n>", "Directory grouping depth; does not limit the scan", positive, 1)
17
+ .option("--top <n>", "Maximum groups and largest files to display", positive, 20)
18
+ .option("--min-size <size>", "Include individual files at least this size (e.g. 10MB, 1GiB)", parseDiskSize)
19
+ .option("--apparent", "Rank and filter by file lengths instead of allocated blocks")
20
+ .option("--json", "Emit the versioned disk usage report")
21
+ .action((path, options) => {
22
+ try {
23
+ const { json, ...filters } = options;
24
+ const report = diskUsage(path, filters);
25
+ if (json) {
26
+ emit.config({ format: "json" });
27
+ emit.data(report);
28
+ }
29
+ else
30
+ emit.text(renderDiskUsage(report));
31
+ if (!report.complete)
32
+ emit.setExitCode(1);
33
+ }
34
+ catch (error) {
35
+ emit.error({
36
+ code: "disk_usage_failed",
37
+ message: error instanceof Error ? error.message : String(error),
38
+ });
39
+ emit.setExitCode(1);
40
+ }
41
+ });
42
+ }
43
+ export function renderDiskUsage(report) {
44
+ const bytes = (value) => value >= 1e9
45
+ ? `${(value / 1e9).toFixed(2)} GB`
46
+ : value >= 1e6
47
+ ? `${(value / 1e6).toFixed(2)} MB`
48
+ : value >= 1e3
49
+ ? `${(value / 1e3).toFixed(2)} KB`
50
+ : `${value} B`;
51
+ const size = (row) => bytes(report.measurement === "allocated"
52
+ ? (row.allocated_bytes ?? row.apparent_bytes)
53
+ : row.apparent_bytes);
54
+ const lines = [
55
+ `Disk usage: ${report.root}`,
56
+ `${size(report.totals)} in ${report.totals.files.toLocaleString()} regular files (${report.measurement})`,
57
+ `File lengths: ${bytes(report.totals.apparent_bytes)}. Directory metadata and symlinks are excluded.`,
58
+ "",
59
+ "Git state:",
60
+ ];
61
+ for (const [state, totals] of Object.entries(report.git))
62
+ if (totals.files)
63
+ lines.push(`${size(totals).padStart(12)} ${String(totals.files).padStart(8)} files ${state}`);
64
+ lines.push("", `Largest ${report.filters.group} groups (${report.groups.length} of ${report.group_count}):`);
65
+ for (const row of report.groups)
66
+ lines.push(`${size(row).padStart(12)} ${String(row.files).padStart(8)} files ${row.path}`);
67
+ if (report.filters.group !== "files") {
68
+ lines.push("", "Largest files:");
69
+ for (const row of report.largest_files)
70
+ lines.push(`${size(row).padStart(12)} ${row.git.padEnd(14)} ${row.path}`);
71
+ }
72
+ if (report.skipped.symlinks || report.skipped.special_files || report.skipped.excluded)
73
+ lines.push("", `Skipped: ${report.skipped.symlinks} symlinks, ${report.skipped.special_files} special files, ${report.skipped.excluded} excluded entries.`);
74
+ if (report.issues.length) {
75
+ lines.push("", "Incomplete scan:");
76
+ for (const issue of report.issues)
77
+ lines.push(`${issue.path}: ${issue.message}`);
78
+ }
79
+ return `${lines.join("\n")}\n`;
80
+ }
@@ -24,6 +24,14 @@ export interface Check {
24
24
  export declare function registerDoctorCommand(program: Command, emit: EmitContext): void;
25
25
  export declare function runChecks(): Promise<Check[]>;
26
26
  export declare function codexWslBridgeCheck(status: CodexWslBridgeStatus | null, recentMidFlightCount?: number): Check | null;
27
+ /**
28
+ * Hook runs that started but never finished (ADR 0197). Codex kills a hook at
29
+ * its timeout and the tool proceeds without that hook's checks; the start
30
+ * marker is the only trace such a run leaves.
31
+ */
32
+ export declare function codexHookRunsCheck(coordRoot: string | null, nowMs?: number, isAlive?: (pid: number) => boolean): Check | null;
33
+ /** Report the per-project Windows bridge opt-in, or an invalid value init ignored. */
34
+ export declare function codexWindowsBridgeCheck(coordRoot: string | null): Check | null;
27
35
  /** Count bounded, privacy-safe recovery diagnostics so a healthy environment
28
36
  * now does not hide an intermittent Codex-to-WSL identity drop. */
29
37
  export declare function countRecentCodexMidFlightOnboardings(coordRoot: string, nowMs?: number, windowMs?: number): number;
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AASnD,OAAO,EACL,KAAK,4BAA4B,EAElC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,mCAAmC,CAAC;AAW3C,KAAK,QAAQ,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmD/E;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,CA2BlD;AAgCD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,oBAAoB,GAAG,IAAI,EACnC,oBAAoB,SAAI,GACvB,KAAK,GAAG,IAAI,CAed;AAID;mEACmE;AACnE,wBAAgB,oCAAoC,CAClD,SAAS,EAAE,MAAM,EACjB,KAAK,SAAa,EAClB,QAAQ,SAAqC,GAC5C,MAAM,CA0CR;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,4BAA4B,GAAG,KAAK,CAoBnF"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AASnD,OAAO,EACL,KAAK,4BAA4B,EAElC,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,mCAAmC,CAAC;AAY3C,KAAK,QAAQ,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAOD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAmD/E;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC,CA+BlD;AAgCD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,oBAAoB,GAAG,IAAI,EACnC,oBAAoB,SAAI,GACvB,KAAK,GAAG,IAAI,CAed;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,SAAa,EAClB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GACjC,KAAK,GAAG,IAAI,CAad;AAED,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,CAmB9E;AAID;mEACmE;AACnE,wBAAgB,oCAAoC,CAClD,SAAS,EAAE,MAAM,EACjB,KAAK,SAAa,EAClB,QAAQ,SAAqC,GAC5C,MAAM,CA0CR;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,4BAA4B,GAAG,KAAK,CAoBnF"}