harnery 0.43.0 → 0.44.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 (278) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +41 -48
  4. package/dist/commands/browse.d.ts.map +1 -1
  5. package/dist/commands/browse.js +38 -11
  6. package/dist/commands/diagnostics.d.ts.map +1 -1
  7. package/dist/commands/diagnostics.js +10 -0
  8. package/dist/commands/doctor.d.ts +12 -0
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +22 -7
  11. package/dist/commands/governor.js +22 -10
  12. package/dist/commands/inbox.d.ts +1 -1
  13. package/dist/commands/inbox.d.ts.map +1 -1
  14. package/dist/commands/inbox.js +55 -2
  15. package/dist/commands/rm.d.ts.map +1 -1
  16. package/dist/commands/rm.js +28 -4
  17. package/dist/commands/servers.d.ts.map +1 -1
  18. package/dist/commands/servers.js +16 -1
  19. package/dist/commands/supervisor.d.ts.map +1 -1
  20. package/dist/commands/supervisor.js +7 -2
  21. package/dist/commands/toc.d.ts +14 -0
  22. package/dist/commands/toc.d.ts.map +1 -1
  23. package/dist/commands/toc.js +7 -4
  24. package/dist/commands/workflow.js +24 -30
  25. package/dist/core/agents/live-authority-v3.d.ts +1 -0
  26. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  27. package/dist/core/agents/live-authority-v3.js +11 -0
  28. package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
  29. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  30. package/dist/core/agents/live-lifecycle-v3.js +28 -7
  31. package/dist/core/agents/mailbox.d.ts +9 -8
  32. package/dist/core/agents/mailbox.d.ts.map +1 -1
  33. package/dist/core/agents/mailbox.js +24 -17
  34. package/dist/core/agents/render/prompt-context.d.ts +2 -0
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +8 -12
  37. package/dist/core/agents/render/session-context.d.ts +2 -8
  38. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  39. package/dist/core/agents/render/session-context.js +7 -30
  40. package/dist/core/agents/rules/stop-hook.js +3 -3
  41. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  42. package/dist/core/agents/state/heartbeat-writer.js +25 -14
  43. package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
  44. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  45. package/dist/core/agents/state/pidmap.d.ts.map +1 -1
  46. package/dist/core/agents/state/pidmap.js +5 -0
  47. package/dist/core/agents/state/proc-start.d.ts +19 -0
  48. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  49. package/dist/core/agents/state/proc-start.js +41 -0
  50. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  51. package/dist/core/agents/state/stale-sweep.js +180 -70
  52. package/dist/core/agents/state/unchanged-file.d.ts +44 -0
  53. package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
  54. package/dist/core/agents/state/unchanged-file.js +390 -0
  55. package/dist/core/context/index.d.ts +22 -10
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +381 -119
  58. package/dist/core/context/observation.d.ts +26 -0
  59. package/dist/core/context/observation.d.ts.map +1 -0
  60. package/dist/core/context/observation.js +11 -0
  61. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  62. package/dist/core/diagnostics/bundle.js +25 -14
  63. package/dist/core/diagnostics/path-identity.d.ts +3 -0
  64. package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
  65. package/dist/core/diagnostics/path-identity.js +12 -0
  66. package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
  67. package/dist/core/diagnostics/sanitize.js +4 -3
  68. package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
  69. package/dist/core/events/v3/authority-outbox.js +29 -0
  70. package/dist/core/events/v3/generation-custody.d.ts +5 -0
  71. package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
  72. package/dist/core/events/v3/generation-custody.js +50 -0
  73. package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
  74. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  75. package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
  76. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  77. package/dist/core/events/v3/producers/recorder.js +77 -64
  78. package/dist/core/governor/planning.d.ts.map +1 -1
  79. package/dist/core/governor/planning.js +14 -1
  80. package/dist/core/governor/service-read.d.ts +10 -2
  81. package/dist/core/governor/service-read.d.ts.map +1 -1
  82. package/dist/core/governor/service-read.js +105 -22
  83. package/dist/core/governor/service.d.ts +20 -2
  84. package/dist/core/governor/service.d.ts.map +1 -1
  85. package/dist/core/governor/service.js +72 -47
  86. package/dist/core/hooks/cli.js +33 -5
  87. package/dist/core/inbox/contract.d.ts +2 -1
  88. package/dist/core/inbox/contract.d.ts.map +1 -1
  89. package/dist/core/inbox/index.d.ts +2 -0
  90. package/dist/core/inbox/index.d.ts.map +1 -1
  91. package/dist/core/inbox/index.js +2 -0
  92. package/dist/core/inbox/peer-messages.d.ts +37 -0
  93. package/dist/core/inbox/peer-messages.d.ts.map +1 -0
  94. package/dist/core/inbox/peer-messages.js +72 -0
  95. package/dist/core/inbox/service.d.ts +8 -1
  96. package/dist/core/inbox/service.d.ts.map +1 -1
  97. package/dist/core/inbox/service.js +67 -21
  98. package/dist/core/inbox/targets.d.ts +22 -0
  99. package/dist/core/inbox/targets.d.ts.map +1 -0
  100. package/dist/core/inbox/targets.js +108 -0
  101. package/dist/core/inbox/watcher.js +7 -3
  102. package/dist/core/journal/index.d.ts.map +1 -1
  103. package/dist/core/journal/index.js +17 -2
  104. package/dist/core/resources/contract.d.ts +3 -0
  105. package/dist/core/resources/contract.d.ts.map +1 -1
  106. package/dist/core/resources/service-status.d.ts +14 -1
  107. package/dist/core/resources/service-status.d.ts.map +1 -1
  108. package/dist/core/resources/service-status.js +23 -13
  109. package/dist/core/resources/service.d.ts +9 -1
  110. package/dist/core/resources/service.d.ts.map +1 -1
  111. package/dist/core/resources/service.js +47 -27
  112. package/dist/core/resources/status.d.ts +2 -5
  113. package/dist/core/resources/status.d.ts.map +1 -1
  114. package/dist/core/resources/status.js +36 -7
  115. package/dist/core/servers/index.d.ts +24 -13
  116. package/dist/core/servers/index.d.ts.map +1 -1
  117. package/dist/core/servers/index.js +132 -92
  118. package/dist/core/servers/tunnels.d.ts +3 -5
  119. package/dist/core/servers/tunnels.d.ts.map +1 -1
  120. package/dist/core/servers/tunnels.js +3 -9
  121. package/dist/core/supervisor/contract.d.ts +12 -0
  122. package/dist/core/supervisor/contract.d.ts.map +1 -1
  123. package/dist/core/supervisor/explanations.d.ts.map +1 -1
  124. package/dist/core/supervisor/explanations.js +2 -1
  125. package/dist/core/supervisor/findings.d.ts.map +1 -1
  126. package/dist/core/supervisor/findings.js +54 -9
  127. package/dist/core/supervisor/service.d.ts +9 -1
  128. package/dist/core/supervisor/service.d.ts.map +1 -1
  129. package/dist/core/supervisor/service.js +59 -29
  130. package/dist/core/supervisor/services.d.ts.map +1 -1
  131. package/dist/core/supervisor/services.js +22 -1
  132. package/dist/core/supervisor/status.d.ts +17 -2
  133. package/dist/core/supervisor/status.d.ts.map +1 -1
  134. package/dist/core/supervisor/status.js +70 -12
  135. package/dist/core/supervisor/timeline.d.ts.map +1 -1
  136. package/dist/core/supervisor/timeline.js +2 -1
  137. package/dist/core/work/state.js +7 -7
  138. package/dist/core/workflow/engine.d.ts.map +1 -1
  139. package/dist/core/workflow/engine.js +105 -32
  140. package/dist/core/workflow/failure-evidence.d.ts +16 -0
  141. package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
  142. package/dist/core/workflow/failure-evidence.js +114 -0
  143. package/dist/core/workflow/index.d.ts +1 -1
  144. package/dist/core/workflow/index.d.ts.map +1 -1
  145. package/dist/core/workflow/index.js +1 -1
  146. package/dist/core/workflow/live-session-v3.d.ts +6 -0
  147. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  148. package/dist/core/workflow/live-session-v3.js +126 -5
  149. package/dist/core/workflow/proof.d.ts +5 -5
  150. package/dist/core/workflow/proof.d.ts.map +1 -1
  151. package/dist/core/workflow/proof.js +115 -20
  152. package/dist/core/workflow/run-state.d.ts +3 -1
  153. package/dist/core/workflow/run-state.d.ts.map +1 -1
  154. package/dist/core/workflow/run-state.js +31 -27
  155. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  156. package/dist/core/workflow/spawn-claude.js +19 -0
  157. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  158. package/dist/core/workflow/spawn-codex.js +13 -0
  159. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  160. package/dist/core/workflow/spawn-cursor.js +19 -0
  161. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  162. package/dist/core/workflow/spawn-opencode.js +14 -0
  163. package/dist/core/workflow/types.d.ts +64 -17
  164. package/dist/core/workflow/types.d.ts.map +1 -1
  165. package/dist/core/workflow/workspaces/index.d.ts +0 -1
  166. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  167. package/dist/core/workflow/workspaces/index.js +0 -1
  168. package/dist/core/workflow/workspaces/leases.d.ts +3 -1
  169. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  170. package/dist/core/workflow/workspaces/leases.js +30 -16
  171. package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
  172. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  173. package/dist/core/workflow/workspaces/local-git.js +270 -6
  174. package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
  175. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
  176. package/dist/core/workflow/workspaces/ownership.js +194 -0
  177. package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
  178. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  179. package/dist/core/workflow/workspaces/reclaim.js +14 -0
  180. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  181. package/dist/core/workflow/workspaces/recovery.js +21 -13
  182. package/dist/core/workflow/workspaces/state.d.ts +1 -0
  183. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  184. package/dist/core/workflow/workspaces/state.js +21 -1
  185. package/dist/lib/browser/windows-chrome.d.ts +100 -12
  186. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  187. package/dist/lib/browser/windows-chrome.js +262 -73
  188. package/dist/lib/docs-links.d.ts.map +1 -1
  189. package/dist/lib/docs-links.js +45 -2
  190. package/dist/lib/guarded-remove.d.ts +7 -0
  191. package/dist/lib/guarded-remove.d.ts.map +1 -1
  192. package/dist/lib/guarded-remove.js +106 -12
  193. package/dist/lib/identities/assume.d.ts.map +1 -1
  194. package/dist/lib/identities/assume.js +4 -0
  195. package/dist/lib/zip/index.d.ts +41 -0
  196. package/dist/lib/zip/index.d.ts.map +1 -0
  197. package/dist/lib/zip/index.js +398 -0
  198. package/package.json +7 -1
  199. package/src/commands/agents.ts +54 -63
  200. package/src/commands/browse.ts +50 -14
  201. package/src/commands/diagnostics.ts +10 -0
  202. package/src/commands/doctor.ts +26 -7
  203. package/src/commands/governor.ts +23 -10
  204. package/src/commands/inbox.ts +52 -2
  205. package/src/commands/rm.ts +43 -4
  206. package/src/commands/servers.ts +16 -4
  207. package/src/commands/supervisor.ts +7 -2
  208. package/src/commands/toc.ts +14 -6
  209. package/src/commands/workflow.ts +30 -30
  210. package/src/core/agents/live-authority-v3.ts +17 -0
  211. package/src/core/agents/live-lifecycle-v3.ts +52 -14
  212. package/src/core/agents/mailbox.ts +26 -17
  213. package/src/core/agents/render/prompt-context.ts +10 -12
  214. package/src/core/agents/render/session-context.ts +9 -32
  215. package/src/core/agents/rules/stop-hook.ts +3 -3
  216. package/src/core/agents/state/heartbeat-writer.ts +29 -12
  217. package/src/core/agents/state/live-coordination-writer.ts +29 -0
  218. package/src/core/agents/state/pidmap.ts +13 -0
  219. package/src/core/agents/state/proc-start.ts +57 -0
  220. package/src/core/agents/state/stale-sweep.ts +247 -82
  221. package/src/core/agents/state/unchanged-file.ts +456 -0
  222. package/src/core/context/index.ts +446 -136
  223. package/src/core/context/observation.ts +29 -0
  224. package/src/core/diagnostics/bundle.ts +23 -18
  225. package/src/core/diagnostics/path-identity.ts +15 -0
  226. package/src/core/diagnostics/sanitize.ts +4 -3
  227. package/src/core/events/v3/authority-outbox.ts +56 -0
  228. package/src/core/events/v3/generation-custody.ts +59 -0
  229. package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
  230. package/src/core/events/v3/producers/recorder.ts +94 -68
  231. package/src/core/governor/planning.ts +20 -8
  232. package/src/core/governor/service-read.ts +128 -20
  233. package/src/core/governor/service.ts +119 -42
  234. package/src/core/hooks/cli.ts +33 -5
  235. package/src/core/inbox/contract.ts +2 -1
  236. package/src/core/inbox/index.ts +2 -0
  237. package/src/core/inbox/peer-messages.ts +94 -0
  238. package/src/core/inbox/service.ts +88 -22
  239. package/src/core/inbox/targets.ts +148 -0
  240. package/src/core/inbox/watcher.ts +8 -9
  241. package/src/core/journal/index.ts +19 -2
  242. package/src/core/resources/contract.ts +4 -0
  243. package/src/core/resources/service-status.ts +35 -12
  244. package/src/core/resources/service.ts +71 -24
  245. package/src/core/resources/status.ts +57 -8
  246. package/src/core/servers/index.ts +177 -91
  247. package/src/core/servers/tunnels.ts +3 -11
  248. package/src/core/supervisor/contract.ts +13 -0
  249. package/src/core/supervisor/explanations.ts +4 -1
  250. package/src/core/supervisor/findings.ts +68 -10
  251. package/src/core/supervisor/service.ts +88 -26
  252. package/src/core/supervisor/services.ts +26 -8
  253. package/src/core/supervisor/status.ts +89 -12
  254. package/src/core/supervisor/timeline.ts +2 -1
  255. package/src/core/work/state.ts +7 -7
  256. package/src/core/workflow/engine.ts +131 -49
  257. package/src/core/workflow/failure-evidence.ts +141 -0
  258. package/src/core/workflow/index.ts +0 -1
  259. package/src/core/workflow/live-session-v3.ts +146 -5
  260. package/src/core/workflow/proof.ts +134 -26
  261. package/src/core/workflow/run-state.ts +31 -29
  262. package/src/core/workflow/spawn-claude.ts +18 -0
  263. package/src/core/workflow/spawn-codex.ts +13 -0
  264. package/src/core/workflow/spawn-cursor.ts +18 -0
  265. package/src/core/workflow/spawn-opencode.ts +14 -0
  266. package/src/core/workflow/types.ts +51 -17
  267. package/src/core/workflow/workspaces/index.ts +0 -1
  268. package/src/core/workflow/workspaces/leases.ts +48 -17
  269. package/src/core/workflow/workspaces/local-git.ts +315 -5
  270. package/src/core/workflow/workspaces/ownership.ts +232 -0
  271. package/src/core/workflow/workspaces/reclaim.ts +58 -0
  272. package/src/core/workflow/workspaces/recovery.ts +25 -13
  273. package/src/core/workflow/workspaces/state.ts +20 -0
  274. package/src/lib/browser/windows-chrome.ts +346 -86
  275. package/src/lib/docs-links.ts +49 -2
  276. package/src/lib/guarded-remove.ts +132 -14
  277. package/src/lib/identities/assume.ts +4 -0
  278. package/src/lib/zip/index.ts +483 -0
@@ -5,18 +5,19 @@ import { join } from "node:path";
5
5
  /**
6
6
  * Sign-in through the Windows host's Google Chrome when running under WSL.
7
7
  *
8
- * Some sign-in flows (X is the reported case) reject every browser running
9
- * inside WSL, including a plain Linux Google Chrome with nothing attached:
10
- * the sign-in request comes back "429 Too Many Requests" while the same
11
- * account signs in from the Windows desktop browser without trouble. The
12
- * Windows Chrome is the client those flows accept, so `browse --login
13
- * --plain` under WSL opens it on a dedicated Windows-side profile. The
14
- * person signs in there with nothing attached. Afterwards the profile is
15
- * reopened headless for a moment, its cookies for the sign-in site are read
16
- * through the DevTools protocol, and they are merged into the shared cookie
17
- * store that later `browse` runs attach. The Windows profile cannot be shared
18
- * with Linux Chromium directly because Windows encrypts its cookie database
19
- * with keys only Windows can read.
8
+ * Some sign-in flows reject every browser running inside WSL, including a
9
+ * plain Linux Google Chrome with nothing attached: the sign-in request comes
10
+ * back "429 Too Many Requests" while the same account signs in from the
11
+ * Windows desktop browser without trouble. The Windows Chrome is the client
12
+ * those flows accept, so `browse --login --plain` under WSL opens it on a
13
+ * dedicated Windows-side profile. The person signs in there with nothing
14
+ * attached. Afterwards the profile is reopened headless for a moment with its
15
+ * last session restored, its cookies for the sign-in site (browser-session
16
+ * cookies included) and its open pages on that site are read through the
17
+ * DevTools protocol, and the cookies are merged into the shared cookie store
18
+ * that later `browse` runs attach. The Windows profile cannot be shared with
19
+ * Linux Chromium directly because Windows encrypts its cookie database with
20
+ * keys only Windows can read.
20
21
  *
21
22
  * Under WSL2's default NAT networking the Windows loopback port is not
22
23
  * reachable from Linux, so the DevTools conversation runs inside a short
@@ -79,96 +80,284 @@ export function cookiesForDomain(cdpCookies, domain) {
79
80
  }));
80
81
  }
81
82
  /**
82
- * PowerShell that does the whole read on the Windows side: start Chrome
83
- * headless on the profile, wait for the DevToolsActivePort file it writes,
84
- * read every cookie with `Storage.getCookies`, write the raw response to
85
- * stdout, and close the browser. WSL never touches the profile folder, which
86
- * it may not be able to list. Every wait carries a deadline.
83
+ * Regular expression, valid in both JavaScript and .NET, that matches a Chrome
84
+ * command line running on `profileWindows`. It matches the `--user-data-dir`
85
+ * argument exactly, with or without quotes around the path, so `...\profile`
86
+ * never matches `...\profile-2`. Quotes are written as `\x22` so the pattern
87
+ * survives being passed to PowerShell as an argument. Case-insensitive use is
88
+ * intended, as Windows paths are.
87
89
  */
88
- export function harvestScript() {
90
+ export function profileCommandLinePattern(profileWindows) {
91
+ const path = profileWindows.replace(/\\+$/, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92
+ return `--user-data-dir=\\x22?${path}\\\\?(\\x22|\\s|$)`;
93
+ }
94
+ /** PowerShell function listing the Chrome processes whose command line matches `$Pattern`. */
95
+ const PROFILE_PROCESS_FUNCTION = "function Get-ProfileChrome { @(Get-CimInstance Win32_Process -Filter \"Name='chrome.exe'\" | Where-Object { $_.CommandLine -and $_.CommandLine -match $Pattern }) }";
96
+ /**
97
+ * PowerShell that throws while any Chrome is still running on the profile: a
98
+ * second Chrome on the same profile hands its work to the first and exits.
99
+ */
100
+ const REFUSE_OPEN_PROFILE_LINES = [
101
+ PROFILE_PROCESS_FUNCTION,
102
+ "$alive = Get-ProfileChrome",
103
+ "if ($alive.Count -gt 0) { throw \"Chrome is still running on the sign-in profile (process $(($alive | ForEach-Object { $_.ProcessId }) -join ', ')). Close that window and try again.\" }",
104
+ ];
105
+ /**
106
+ * PowerShell that starts Chrome headless on `$Profile` with `extraArgs`, waits
107
+ * for the DevToolsActivePort file it writes, and connects to the browser
108
+ * endpoint, defining `Send`, `Recv`, and `Wait-Reply` for the conversation.
109
+ * It refuses to start while any Chrome is still running on the profile,
110
+ * which would otherwise surface as a confusing startup failure.
111
+ */
112
+ function headlessSessionLines(extraArgs) {
113
+ const quoted = extraArgs.map((arg) => `'${arg}'`).join(", ");
89
114
  return [
90
- "param([string]$Chrome, [string]$Profile, [int]$StartupSeconds = 30)",
91
- "$ErrorActionPreference = 'Stop'",
115
+ ...REFUSE_OPEN_PROFILE_LINES,
92
116
  "$portFile = Join-Path $Profile 'DevToolsActivePort'",
93
117
  "if (Test-Path $portFile) { Remove-Item $portFile -Force }",
94
- "$args = @('--headless=new', '--no-first-run', '--no-default-browser-check', '--remote-debugging-port=0', \"--user-data-dir=$Profile\", 'about:blank')",
95
- "$proc = Start-Process -FilePath $Chrome -ArgumentList $args -PassThru -WindowStyle Hidden",
118
+ `$chromeArgs = @('--headless=new', '--no-first-run', '--no-default-browser-check', ${quoted ? `${quoted}, ` : ""}'--remote-debugging-port=0', "--user-data-dir=\`"$Profile\`"", 'about:blank')`,
119
+ "$proc = Start-Process -FilePath $Chrome -ArgumentList $chromeArgs -PassThru -WindowStyle Hidden",
96
120
  "$deadline = (Get-Date).AddSeconds($StartupSeconds)",
97
- "while (-not (Test-Path $portFile)) { if ((Get-Date) -gt $deadline) { throw 'Windows Chrome did not open its DevTools port in time.' }; if ($proc.HasExited) { throw 'Windows Chrome exited before opening its DevTools port.' }; Start-Sleep -Milliseconds 250 }",
121
+ "while (-not (Test-Path $portFile)) { if ((Get-Date) -gt $deadline) { throw 'Windows Chrome did not open its DevTools port in time.' }; if ($proc.HasExited) { throw 'Windows Chrome exited before opening its DevTools port; another Chrome may have opened on the sign-in profile.' }; Start-Sleep -Milliseconds 250 }",
98
122
  "Start-Sleep -Milliseconds 200",
99
123
  "$lines = Get-Content $portFile",
100
124
  "$port = [int]$lines[0]; $path = $lines[1]",
101
125
  "$ws = New-Object System.Net.WebSockets.ClientWebSocket",
102
- "$cts = New-Object System.Threading.CancellationTokenSource 20000",
126
+ "$cts = New-Object System.Threading.CancellationTokenSource 30000",
103
127
  '$ws.ConnectAsync([Uri]("ws://127.0.0.1:$port$path"), $cts.Token).Wait()',
104
- "function Send($obj) { $b = [Text.Encoding]::UTF8.GetBytes(($obj | ConvertTo-Json -Compress)); $ws.SendAsync([ArraySegment[byte]]$b, 'Text', $true, $cts.Token).Wait() }",
128
+ "function Send($obj) { $b = [Text.Encoding]::UTF8.GetBytes(($obj | ConvertTo-Json -Compress -Depth 6)); $ws.SendAsync([ArraySegment[byte]]$b, 'Text', $true, $cts.Token).Wait() }",
105
129
  "function Recv() { $buf = New-Object byte[] 65536; $ms = New-Object IO.MemoryStream; do { $r = $ws.ReceiveAsync([ArraySegment[byte]]$buf, $cts.Token).Result; $ms.Write($buf, 0, $r.Count) } while (-not $r.EndOfMessage); [Text.Encoding]::UTF8.GetString($ms.ToArray()) }",
130
+ "function Wait-Reply([int]$n) { while ($true) { $m = Recv; if ($m -match ('\"id\":' + $n + '[,}]')) { return $m } } }",
131
+ ];
132
+ }
133
+ /** PowerShell that closes the headless browser started by `headlessSessionLines`. */
134
+ const CLOSE_HEADLESS_LINES = [
135
+ "try { Send @{ id = 0; method = 'Browser.close' } } catch {}",
136
+ "if (-not $proc.WaitForExit(10000)) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }",
137
+ ];
138
+ /**
139
+ * PowerShell that sets the profile's startup preference to "continue where
140
+ * you left off". Chrome deletes browser-session cookies (cookies with no
141
+ * expiry) when it exits normally unless that preference is set, and many
142
+ * sign-ins keep the signed-in state only in such cookies. With it set, the
143
+ * person can simply close the window and the sign-in survives for the
144
+ * harvest, and the next sign-in window reopens where they left off.
145
+ *
146
+ * The preference is protected against edits to the profile's files, so the
147
+ * script sets it the way the settings page does: it opens the profile
148
+ * headless, loads the settings page, and calls the settings API there. When
149
+ * the profile's preference files already show the value, Chrome is not
150
+ * started. The script prints `{"changed":bool,"value":n}`.
151
+ */
152
+ export function keepSessionScript() {
153
+ return [
154
+ "param([string]$Chrome, [string]$Profile, [string]$Pattern, [int]$StartupSeconds = 30)",
155
+ "$ErrorActionPreference = 'Stop'",
156
+ ...REFUSE_OPEN_PROFILE_LINES,
157
+ "foreach ($name in @('Secure Preferences', 'Preferences')) { $prefsFile = Join-Path (Join-Path $Profile 'Default') $name; if (Test-Path $prefsFile) { try { $prefs = Get-Content -Raw $prefsFile | ConvertFrom-Json; if ($prefs.session.restore_on_startup -eq 1) { [Console]::Out.Write('{\"changed\":false,\"value\":1}'); exit 0 } } catch {} } }",
158
+ ...headlessSessionLines([]),
159
+ "Send @{ id = 1; method = 'Target.createTarget'; params = @{ url = 'chrome://settings/onStartup' } }",
160
+ "$targetId = (ConvertFrom-Json (Wait-Reply 1)).result.targetId",
161
+ "Send @{ id = 2; method = 'Target.attachToTarget'; params = @{ targetId = $targetId; flatten = $true } }",
162
+ "$sessionId = (ConvertFrom-Json (Wait-Reply 2)).result.sessionId",
163
+ "$expr = \"new Promise((resolve) => { const started = Date.now(); const attempt = () => { const api = globalThis.chrome && chrome.settingsPrivate; if (api) { api.setPref('session.restore_on_startup', 1, '', () => api.getPref('session.restore_on_startup', (pref) => resolve(JSON.stringify({ changed: true, value: pref ? pref.value : null })))); } else if (Date.now() - started > 10000) { resolve(JSON.stringify({ changed: false, value: null })); } else { setTimeout(attempt, 100); } }; attempt(); })\"",
164
+ "Send @{ id = 3; sessionId = $sessionId; method = 'Runtime.evaluate'; params = @{ expression = $expr; awaitPromise = $true; returnByValue = $true } }",
165
+ "$value = (ConvertFrom-Json (Wait-Reply 3)).result.result.value",
166
+ "Start-Sleep -Milliseconds 300",
167
+ ...CLOSE_HEADLESS_LINES,
168
+ "[Console]::Out.Write($value)",
169
+ ].join("\r\n");
170
+ }
171
+ /**
172
+ * PowerShell that does the whole read on the Windows side: start Chrome
173
+ * headless on the profile with its last session restored, read every cookie
174
+ * with `Storage.getCookies`, list the open pages with `Target.getTargets`,
175
+ * write both raw responses to stdout as one JSON object, and close the
176
+ * browser. WSL never touches the profile folder, which it may not be able to
177
+ * list. Every wait carries a deadline.
178
+ *
179
+ * Restoring the session keeps browser-session cookies that a forced stop left
180
+ * on disk, and it reopens the tabs the person had open, so the harvest can
181
+ * report the last page they reached on the sign-in site. That page's URL may
182
+ * carry a per-session token that cookies alone do not replace. The page list
183
+ * is polled for up to `$PageSeconds` until a page on `$Domain` appears.
184
+ */
185
+ export function harvestScript() {
186
+ return [
187
+ "param([string]$Chrome, [string]$Profile, [string]$Pattern, [string]$Domain = '', [int]$StartupSeconds = 30, [int]$PageSeconds = 5)",
188
+ "$ErrorActionPreference = 'Stop'",
189
+ ...headlessSessionLines(["--restore-last-session"]),
190
+ "function Test-SiteUrl([string]$u) { if (-not $Domain) { return $false }; try { $h = ([Uri]$u).Host.ToLowerInvariant() } catch { return $false }; return ($h -eq $Domain -or $h.EndsWith('.' + $Domain)) }",
106
191
  "Send @{ id = 1; method = 'Storage.getCookies' }",
107
- "$out = $null",
108
- "while ($null -eq $out) { $m = Recv; if ($m -match '\"id\":1[,}]') { $out = $m } }",
109
- "[Console]::Out.Write($out)",
110
- "try { Send @{ id = 2; method = 'Browser.close' } } catch {}",
111
- "if (-not $proc.WaitForExit(10000)) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }",
192
+ "$cookiesReply = Wait-Reply 1",
193
+ "$pageDeadline = (Get-Date).AddSeconds($PageSeconds)",
194
+ "$n = 1",
195
+ "while ($true) { $n++; Send @{ id = $n; method = 'Target.getTargets' }; $targetsReply = Wait-Reply $n; $sitePages = @((ConvertFrom-Json $targetsReply).result.targetInfos | Where-Object { $_.type -eq 'page' -and (Test-SiteUrl $_.url) }); if ($sitePages.Count -gt 0 -or (Get-Date) -gt $pageDeadline) { break }; Start-Sleep -Milliseconds 250 }",
196
+ "[Console]::Out.Write('{\"cookies\":' + $cookiesReply + ',\"targets\":' + $targetsReply + '}')",
197
+ ...CLOSE_HEADLESS_LINES,
112
198
  ].join("\r\n");
113
199
  }
114
200
  /**
115
- * Reopen the Windows profile headless, read its cookies for `domain`, and
116
- * close it. Returns the cookies in the store's shape.
201
+ * URLs of the open pages on the sign-in site, in the order Chrome lists them,
202
+ * without duplicates.
117
203
  */
118
- export function harvestWindowsChromeCookies(input) {
204
+ export function sitePages(targets, domain) {
205
+ const pages = [];
206
+ for (const target of targets) {
207
+ if (target.type !== "page")
208
+ continue;
209
+ let host;
210
+ try {
211
+ host = new URL(target.url).hostname;
212
+ }
213
+ catch {
214
+ continue;
215
+ }
216
+ if (host && cookieInDomain(host, domain) && !pages.includes(target.url))
217
+ pages.push(target.url);
218
+ }
219
+ return pages;
220
+ }
221
+ /** Parse the harvest script's stdout. */
222
+ export function parseHarvestOutput(stdout, domain) {
223
+ const parsed = JSON.parse(stdout);
224
+ const cookieList = parsed.cookies?.result?.cookies;
225
+ if (!cookieList)
226
+ throw new Error(`Windows Chrome returned no cookie list: ${parsed.cookies?.error?.message ?? "unknown response"}`);
227
+ return {
228
+ cookies: cookiesForDomain(cookieList, domain),
229
+ pages: sitePages(parsed.targets?.result?.targetInfos ?? [], domain),
230
+ };
231
+ }
232
+ /** Write a PowerShell script to a temp file and run it with `-File`. */
233
+ function runPowerShellScript(name, script, args, timeoutMs) {
119
234
  const scriptDir = join(tmpdir(), "harnery-windows-chrome");
120
235
  mkdirSync(scriptDir, { recursive: true });
121
- const scriptLinux = join(scriptDir, `harvest-cookies-${process.pid}.ps1`);
122
- writeFileSync(scriptLinux, harvestScript(), { mode: 0o600 });
236
+ const scriptLinux = join(scriptDir, `${name}-${process.pid}.ps1`);
237
+ writeFileSync(scriptLinux, script, { mode: 0o600 });
123
238
  try {
124
239
  const scriptWindows = spawnSync("wslpath", ["-w", scriptLinux], {
125
240
  encoding: "utf8",
126
241
  }).stdout.trim();
127
- const result = spawnSync("powershell.exe", [
128
- "-NoProfile",
129
- "-ExecutionPolicy",
130
- "Bypass",
131
- "-File",
132
- scriptWindows,
133
- "-Chrome",
134
- input.chromeWindows,
135
- "-Profile",
136
- input.profileWindows,
137
- "-StartupSeconds",
138
- String(input.startupTimeoutSeconds ?? 30),
139
- ], { encoding: "utf8", timeout: 90_000, maxBuffer: 64 * 1024 * 1024 });
140
- if (result.status !== 0) {
141
- throw new Error(`Reading cookies from Windows Chrome failed: ${(result.stderr || result.stdout || "no output").trim().slice(0, 400)}`);
142
- }
143
- const parsed = JSON.parse(result.stdout);
144
- if (!parsed.result?.cookies)
145
- throw new Error(`Windows Chrome returned no cookie list: ${parsed.error?.message ?? "unknown response"}`);
146
- return cookiesForDomain(parsed.result.cookies, input.domain);
242
+ const result = spawnSync("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptWindows, ...args], { encoding: "utf8", timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
243
+ return {
244
+ status: result.status,
245
+ stdout: result.stdout ?? "",
246
+ stderr: result.stderr ?? "",
247
+ ...(result.error ? { error: result.error } : {}),
248
+ };
147
249
  }
148
250
  finally {
149
251
  rmSync(scriptLinux, { force: true });
150
252
  }
151
253
  }
152
254
  /**
153
- * Close any Chrome still running on `profileWindows`, for when the person ends
154
- * the sign-in from the terminal while the window is open. The profile must be
155
- * closed before its cookies can be read. Closing the window first lets Chrome
156
- * write recent cookies to disk; a forced stop is the fallback after 15 s.
255
+ * Reopen the Windows profile headless with its last session restored, read
256
+ * its cookies for `domain` and its open pages on that site, and close it.
257
+ * The profile must already be closed; see `stopWindowsChromeProfile`.
258
+ */
259
+ export function harvestWindowsChromeCookies(input) {
260
+ const result = runPowerShellScript("harvest-cookies", harvestScript(), [
261
+ "-Chrome",
262
+ input.chromeWindows,
263
+ "-Profile",
264
+ input.profileWindows,
265
+ "-Pattern",
266
+ profileCommandLinePattern(input.profileWindows),
267
+ "-Domain",
268
+ input.domain,
269
+ "-StartupSeconds",
270
+ String(input.startupTimeoutSeconds ?? 30),
271
+ "-PageSeconds",
272
+ String(input.pageTimeoutSeconds ?? 5),
273
+ ], 90_000);
274
+ if (result.status !== 0) {
275
+ throw new Error(`Reading cookies from Windows Chrome failed: ${(result.stderr || result.stdout || result.error?.message || "no output").trim().slice(0, 400)}`);
276
+ }
277
+ return parseHarvestOutput(result.stdout, input.domain);
278
+ }
279
+ /**
280
+ * Set the profile to keep browser-session cookies across a normal exit; see
281
+ * `keepSessionScript`. Throws when Chrome is already open on the profile,
282
+ * since a second sign-in window would hand off to it. Any other failure
283
+ * returns `kept: false` so the caller can warn and continue.
157
284
  */
158
- export function stopWindowsChromeProfile(profileWindows) {
159
- const escaped = profileWindows.replace(/'/g, "''");
160
- const select = `Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | Where-Object { $_.CommandLine -like '*${escaped}*' }`;
161
- spawnSync("powershell.exe", [
162
- "-NoProfile",
163
- "-Command",
164
- [
165
- `$ids = @(${select} | ForEach-Object { $_.ProcessId })`,
166
- "foreach ($id in $ids) { $p = Get-Process -Id $id -ErrorAction SilentlyContinue; if ($p -and $p.MainWindowHandle -ne 0) { [void]$p.CloseMainWindow() } }",
167
- "$deadline = (Get-Date).AddSeconds(15)",
168
- `while ((Get-Date) -lt $deadline -and @(${select}).Count -gt 0) { Start-Sleep -Milliseconds 250 }`,
169
- `${select} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`,
170
- ].join("; "),
171
- ], { encoding: "utf8", timeout: 45_000 });
285
+ export function keepWindowsChromeSession(input) {
286
+ const result = runPowerShellScript("keep-session", keepSessionScript(), [
287
+ "-Chrome",
288
+ input.chromeWindows,
289
+ "-Profile",
290
+ input.profileWindows,
291
+ "-Pattern",
292
+ profileCommandLinePattern(input.profileWindows),
293
+ "-StartupSeconds",
294
+ String(input.startupTimeoutSeconds ?? 30),
295
+ ], 90_000);
296
+ const detail = (result.stderr || result.stdout || result.error?.message || "").trim();
297
+ if (result.status !== 0) {
298
+ if (detail.includes("Chrome is still running on the sign-in profile"))
299
+ throw new Error(`Chrome is already open on the sign-in profile ${input.profileWindows}. Close that window and run the sign-in again.`);
300
+ return { kept: false, changed: false };
301
+ }
302
+ try {
303
+ const parsed = JSON.parse(result.stdout);
304
+ return { kept: parsed.value === 1, changed: parsed.changed === true };
305
+ }
306
+ catch {
307
+ return { kept: false, changed: false };
308
+ }
309
+ }
310
+ /**
311
+ * PowerShell that closes every Chrome whose command line matches `$Pattern`. It asks each window to
312
+ * close, repeating so a profile with several windows closes them all, which
313
+ * lets Chrome write its cookies and session to disk. Whatever is still
314
+ * running after `$CloseSeconds` is stopped by force. The script prints
315
+ * `{found, forced, remaining}` as JSON; `remaining` lists process ids that
316
+ * survived even the forced stop.
317
+ */
318
+ export function stopScript() {
319
+ return [
320
+ "param([string]$Pattern, [int]$CloseSeconds = 15)",
321
+ "$ErrorActionPreference = 'Stop'",
322
+ PROFILE_PROCESS_FUNCTION,
323
+ "$found = (Get-ProfileChrome).Count",
324
+ "$deadline = (Get-Date).AddSeconds($CloseSeconds)",
325
+ "while ($true) { $procs = Get-ProfileChrome; if ($procs.Count -eq 0 -or (Get-Date) -gt $deadline) { break }; foreach ($c in $procs) { try { $p = Get-Process -Id $c.ProcessId -ErrorAction SilentlyContinue; if ($p -and $p.MainWindowHandle -ne 0) { [void]$p.CloseMainWindow() } } catch {} }; Start-Sleep -Milliseconds 500 }",
326
+ "$forced = 0",
327
+ "foreach ($c in $(Get-ProfileChrome)) { Stop-Process -Id $c.ProcessId -Force -ErrorAction SilentlyContinue; $forced++ }",
328
+ "if ($forced -gt 0) { $killDeadline = (Get-Date).AddSeconds(10); while ((Get-ProfileChrome).Count -gt 0 -and (Get-Date) -lt $killDeadline) { Start-Sleep -Milliseconds 250 } }",
329
+ "$remaining = @(Get-ProfileChrome | ForEach-Object { [int]$_.ProcessId })",
330
+ "[Console]::Out.Write((@{ found = $found; forced = $forced; remaining = $remaining } | ConvertTo-Json -Compress))",
331
+ ].join("\r\n");
332
+ }
333
+ /** Parse the stop script's stdout; throws when a process survived. */
334
+ export function parseStopOutput(stdout, profileWindows) {
335
+ const parsed = JSON.parse(stdout);
336
+ const remaining = parsed.remaining === undefined || parsed.remaining === null
337
+ ? []
338
+ : Array.isArray(parsed.remaining)
339
+ ? parsed.remaining
340
+ : [parsed.remaining];
341
+ if (remaining.length > 0) {
342
+ throw new Error(`Chrome is still running on ${profileWindows} (process ${remaining.join(", ")}) after a forced stop; its cookies cannot be read until it exits.`);
343
+ }
344
+ return { found: Number(parsed.found ?? 0), forced: Number(parsed.forced ?? 0) };
345
+ }
346
+ /**
347
+ * Close every Chrome still running on `profileWindows`, for when the person
348
+ * ends the sign-in from the terminal while the window is open. The profile
349
+ * must be closed before its cookies can be read. Closing the windows lets
350
+ * Chrome write recent cookies to disk; a forced stop is the fallback after
351
+ * `closeSeconds`, and it can lose cookies Chrome had not yet written. Throws
352
+ * when the stop cannot be confirmed, so the harvest never runs against a
353
+ * profile that is still open.
354
+ */
355
+ export function stopWindowsChromeProfile(profileWindows, closeSeconds = 15) {
356
+ const result = runPowerShellScript("stop-profile", stopScript(), ["-Pattern", profileCommandLinePattern(profileWindows), "-CloseSeconds", String(closeSeconds)], (closeSeconds + 60) * 1000);
357
+ if (result.status !== 0) {
358
+ throw new Error(`Closing Chrome on ${profileWindows} failed: ${(result.stderr || result.stdout || result.error?.message || "no output").trim().slice(0, 400)}`);
359
+ }
360
+ return parseStopOutput(result.stdout, profileWindows);
172
361
  }
173
362
  /** Windows form of a `/mnt/<drive>/...` path. */
174
363
  export function wslPathToWindows(linuxPath) {
@@ -1 +1 @@
1
- {"version":3,"file":"docs-links.d.ts","sourceRoot":"","sources":["../../src/lib/docs-links.ts"],"names":[],"mappings":"AAsCA,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GAAG,IAAI,CAIP;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,CAAC;AAE/C,MAAM,MAAM,QAAQ,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,eAAe,GAAG,cAAc,CAAC;AAEhG,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAgGD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA2BlD;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAoED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CA+B5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAuD3D;AA2CD,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;CACvC;AAED,UAAU,UAAU;IAClB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CA+HzD;AAiBD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAyClE"}
1
+ {"version":3,"file":"docs-links.d.ts","sourceRoot":"","sources":["../../src/lib/docs-links.ts"],"names":[],"mappings":"AAsCA,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GAAG,IAAI,CAIP;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,CAAC;AAE/C,MAAM,MAAM,QAAQ,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,eAAe,GAAG,cAAc,CAAC;AAEhG,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAgGD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA2BlD;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAoED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CA+B5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAM/C;AAgDD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAwD3D;AA2CD,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,OAAO,CAAC;IACtB,wEAAwE;IACxE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;CACvC;AAED,UAAU,UAAU;IAClB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,UAAU,CA+HzD;AAiBD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,CAyClE"}
@@ -294,6 +294,48 @@ export function slugify(heading) {
294
294
  .replace(/[^\p{L}\p{N}\s_-]/gu, "")
295
295
  .replace(/\s/g, "-");
296
296
  }
297
+ /** Keep literal code text out of the heading's inline-markup replacements. */
298
+ function shieldHeadingCode(text) {
299
+ let marker = "\u0000";
300
+ while (text.includes(marker))
301
+ marker += "\u0000";
302
+ const spans = [];
303
+ const tokens = /<[^>]+>|`+/g;
304
+ let masked = "";
305
+ let copiedThrough = 0;
306
+ for (let opening = tokens.exec(text); opening; opening = tokens.exec(text)) {
307
+ // An HTML tag starts before its attribute values; their backticks are not
308
+ // code delimiters. Escaped opening backticks are literal punctuation too.
309
+ if (!opening[0].startsWith("`"))
310
+ continue;
311
+ let backslashes = 0;
312
+ for (let i = opening.index - 1; i >= 0 && text[i] === "\\"; i--)
313
+ backslashes++;
314
+ if (backslashes % 2 !== 0)
315
+ continue;
316
+ const closingRuns = /`+/g;
317
+ closingRuns.lastIndex = opening.index + opening[0].length;
318
+ let closing = closingRuns.exec(text);
319
+ while (closing && closing[0].length !== opening[0].length) {
320
+ closing = closingRuns.exec(text);
321
+ }
322
+ if (!closing)
323
+ continue;
324
+ let code = text.slice(opening.index + opening[0].length, closing.index);
325
+ // Code spans remove one padding space at each end, except all-space spans.
326
+ if (code.startsWith(" ") && code.endsWith(" ") && /[^ ]/.test(code)) {
327
+ code = code.slice(1, -1);
328
+ }
329
+ masked += text.slice(copiedThrough, opening.index) + marker + spans.length + marker;
330
+ spans.push(code);
331
+ copiedThrough = closing.index + closing[0].length;
332
+ tokens.lastIndex = copiedThrough;
333
+ }
334
+ return {
335
+ text: masked + text.slice(copiedThrough),
336
+ restore: (filtered) => filtered.replace(new RegExp(`${marker}(\\d+)${marker}`, "g"), (_, index) => spans[Number(index)]),
337
+ };
338
+ }
297
339
  /**
298
340
  * Every fragment a reader can legitimately target in one document: Markdown
299
341
  * heading slugs (ATX and Setext), explicit `{#custom-id}` suffixes, and HTML
@@ -318,7 +360,8 @@ export function collectAnchors(content) {
318
360
  // so only word-boundary underscores are markup. `_` counts as a word
319
361
  // character in JS regex, so \b sits exactly at the space-to-underscore
320
362
  // seam these delimiters occupy.
321
- text = text
363
+ const code = shieldHeadingCode(text);
364
+ text = code.text
322
365
  // Backslash escapes render as the bare character, so `snake\_case`
323
366
  // must anchor exactly like `snake_case`. Unescape before the emphasis
324
367
  // strip or the escaped underscore gains a phantom word boundary.
@@ -327,7 +370,7 @@ export function collectAnchors(content) {
327
370
  .replace(/[*~`]/g, "")
328
371
  .replace(/\b_+|_+\b/g, "")
329
372
  .replace(/<[^>]+>/g, "");
330
- const base = slugify(text);
373
+ const base = slugify(code.restore(text));
331
374
  if (!base)
332
375
  return;
333
376
  const n = seen.get(base) ?? 0;
@@ -11,6 +11,12 @@ export interface RemoveOptions {
11
11
  allowProtected?: string[];
12
12
  /** Directories whose own Git metadata and Harnery state may never be allowed (the live workspace). */
13
13
  liveRoots?: string[];
14
+ /** Command-layer validation of the current caller's ownership of this exact managed artifact root. */
15
+ assertOwnedArtifact?: (root: string) => void;
16
+ /** Inspection bound; values above 10,000 require verified ownership of the exact artifact root. */
17
+ entryLimit?: number;
18
+ /** Remove only verified-clean standalone clones inside the caller's owned artifact. */
19
+ cleanClone?: boolean;
14
20
  }
15
21
  export interface RemovalLink {
16
22
  path: string;
@@ -23,6 +29,7 @@ export interface RemovalReport {
23
29
  root: string;
24
30
  targets: string[];
25
31
  entries: number;
32
+ entry_limit: number;
26
33
  bytes: number;
27
34
  /** Protected paths removed because the caller named them with allowProtected. */
28
35
  protected: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"guarded-remove.d.ts","sourceRoot":"","sources":["../../src/lib/guarded-remove.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sGAAsG;IACtG,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iEAAiE;IACjE,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAKD,uFAAuF;AACvF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,aAAa,EACtB,WAAW,GAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,IAAe,GAClD,aAAa,CAyJf"}
1
+ {"version":3,"file":"guarded-remove.d.ts","sourceRoot":"","sources":["../../src/lib/guarded-remove.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,sGAAsG;IACtG,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,sGAAsG;IACtG,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uFAAuF;IACvF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iEAAiE;IACjE,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAMD,uFAAuF;AACvF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,aAAa,EACtB,WAAW,GAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,IAAe,GAClD,aAAa,CA4Mf"}