@wenathlan/devthink 2.0.15 → 2.0.20

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 (444) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/commands.ts DELETED
@@ -1,1101 +0,0 @@
1
- /** The commands module of the 1.1.90 consolidation: every correlated variation of the background command executors for tabs, files, clipboard, downloads and navigation interned in this one file, so the module family carries one surface without duplicate variations. */
2
-
3
- /* ── Merged from tabscommand.ts: the 1.1.90 consolidation interns the correlated tabscommand logic here, so no variation of the same file lives beside another. ── */
4
- import type {
5
- artifactinventoryentry,
6
- capturename,
7
- cleanuprule,
8
- clipentry,
9
- closedtabrecord,
10
- deeplinkpattern,
11
- downloadrecord,
12
- downloadstate,
13
- linkbatch,
14
- mimefilter,
15
- navpause,
16
- navtrailentry,
17
- netlogrecord,
18
- planprogress,
19
- preconnecttarget,
20
- prefetchplan,
21
- quarantineentry,
22
- ratelimitwindow,
23
- safetyverdict,
24
- scanverdict,
25
- tabgrouprecord,
26
- tablayout,
27
- tabquery,
28
- tabwatchevent,
29
- toolstep,
30
- urlvisit,
31
- } from "./types.js";
32
- import { parseoptions } from "./policy.js";
33
- import { assigntasktab, tasktabs } from "./progress.js";
34
-
35
- /**
36
- * Tabs and windows command logics for reviewed steps.
37
- * Every correlated rule for tab queries, clone detection, group membership, layouts, snapshots, watchers, badges, discard candidates, switcher order, zoom steps and the task tab budget lives in this file.
38
- */
39
-
40
- /** One serializable live tab shape resolved against the browser tab set. */
41
- export interface tabshape {
42
- tabid: number;
43
- url: string;
44
- title: string;
45
- index: number;
46
- windowid: number;
47
- active: boolean;
48
- pinned: boolean;
49
- audible: boolean;
50
- muted: boolean;
51
- discarded: boolean;
52
- }
53
-
54
- /** One serializable live window shape with bounds, state and profile kind. */
55
- export interface windowshape {
56
- windowid: number;
57
- left: number;
58
- top: number;
59
- width: number;
60
- height: number;
61
- state: "normal" | "maximized" | "minimized" | "fullscreen";
62
- incognito: boolean;
63
- focused: boolean;
64
- }
65
-
66
- /** Reads the reviewed tabquery of a tabs and windows command step; null when the step reviews none. */
67
- export function parsetabquery(step: toolstep): tabquery | null {
68
- let options: Record<string, unknown> = {};
69
- try {
70
- options = parseoptions(step);
71
- } catch {
72
- options = {};
73
- }
74
- const value = options.tabquery;
75
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
76
- const query = value as Record<string, unknown>;
77
- return {
78
- ...(typeof query.url === "string" && query.url ? { url: query.url } : {}),
79
- ...(typeof query.title === "string" && query.title ? { title: query.title } : {}),
80
- ...(typeof query.id === "number" && Number.isInteger(query.id) && query.id >= 0 ? { id: query.id } : {}),
81
- ...(typeof query.pattern === "string" && query.pattern ? { pattern: query.pattern } : {}),
82
- };
83
- }
84
-
85
- /** Matches one reviewed wildcard pattern against a url; `*` spans one path segment and `**` spans any part. The walk stays linear on adversarial patterns because a token table with one dynamic programming pass replaces the compiled regular expression a star storm would catastrophically backtrack. */
86
- export function tabpatternmatches(pattern: string, url: string): boolean {
87
- const tokens: Array<{ kind: "literal" | "star" | "doublestar"; text: string }> = [];
88
- let literal = "";
89
- const flush = (): void => {
90
- if (literal !== "") {
91
- tokens.push({ kind: "literal", text: literal });
92
- literal = "";
93
- }
94
- };
95
- for (let index = 0; index < pattern.length; index += 1) {
96
- const character = pattern[index] ?? "";
97
- if (character !== "*") {
98
- literal += character;
99
- continue;
100
- }
101
- if (pattern[index + 1] === "*") {
102
- flush();
103
- tokens.push({ kind: "doublestar", text: "" });
104
- index += 1;
105
- continue;
106
- }
107
- flush();
108
- tokens.push({ kind: "star", text: "" });
109
- }
110
- flush();
111
- let tail: boolean[] = new Array<boolean>(url.length + 1).fill(false);
112
- tail[url.length] = true; /* the empty token tail matches only at the end of the url */
113
- for (let tokenindex = tokens.length - 1; tokenindex >= 0; tokenindex -= 1) {
114
- const token = tokens[tokenindex]!;
115
- const current = new Array<boolean>(url.length + 1).fill(false);
116
- for (let position = url.length; position >= 0; position -= 1) {
117
- if (token.kind === "literal") {
118
- current[position] = url.startsWith(token.text, position) && tail[position + token.text.length] === true;
119
- continue;
120
- }
121
- const crosses = token.kind === "star" && position < url.length && url[position] === "/";
122
- current[position] =
123
- !crosses && (tail[position] === true || (position < url.length && current[position + 1] === true));
124
- }
125
- tail = current;
126
- }
127
- return tail[0] === true;
128
- }
129
-
130
- /** Resolves one reviewed tabquery against the live tab set: every matcher that exists must hold. */
131
- export function querymatches(query: tabquery, tabs: tabshape[]): tabshape[] {
132
- return tabs.filter((tab) => {
133
- if (query.id !== undefined && tab.tabid !== query.id) return false;
134
- if (query.url !== undefined && tab.url !== query.url) return false;
135
- if (query.title !== undefined && !tab.title.toLowerCase().includes(query.title.toLowerCase())) return false;
136
- if (query.pattern !== undefined && !tabpatternmatches(query.pattern, tab.url)) return false;
137
- return true;
138
- });
139
- }
140
-
141
- /** Normalizes one url for clone comparison by dropping the fragment and trailing slashes. */
142
- export function normalizedtaburl(url: string): string {
143
- let normalized = url;
144
- const hash = normalized.indexOf("#");
145
- if (hash >= 0) normalized = normalized.slice(0, hash);
146
- while (normalized.length > 1 && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
147
- return normalized;
148
- }
149
-
150
- /** One clone warning: a normalized url shared by more than one open tab. */
151
- export interface clonewarning {
152
- url: string;
153
- tabids: number[];
154
- }
155
-
156
- /** Detects duplicate tabs by normalized url comparison and returns every url held by more than one tab. */
157
- export function clonetabs(tabs: tabshape[]): clonewarning[] {
158
- const groups = new Map<string, number[]>();
159
- for (const tab of tabs) {
160
- if (!tab.url) continue;
161
- const key = normalizedtaburl(tab.url);
162
- groups.set(key, [...(groups.get(key) ?? []), tab.tabid]);
163
- }
164
- return [...groups.entries()].filter(([, tabids]) => tabids.length > 1).map(([url, tabids]) => ({ url, tabids }));
165
- }
166
-
167
- /** Searches across open tabs by title and url, case insensitive, returning matches in live order. */
168
- export function searchtabmatches(tabs: tabshape[], text: string): tabshape[] {
169
- const needle = text.trim().toLowerCase();
170
- if (!needle) return [];
171
- return tabs.filter((tab) => tab.title.toLowerCase().includes(needle) || tab.url.toLowerCase().includes(needle));
172
- }
173
-
174
- /** Lists the tabs that are playing audio: audible or muted but still playing. */
175
- export function audiotabs(tabs: tabshape[]): tabshape[] {
176
- return tabs.filter((tab) => tab.audible || (tab.muted && tab.audible));
177
- }
178
-
179
- /** Returns the inactive, unpinned and not yet discarded tabs a discardtab step may release. */
180
- export function discardcandidates(tabs: tabshape[]): tabshape[] {
181
- return tabs.filter((tab) => !tab.active && !tab.pinned && !tab.discarded && tab.url.length > 0);
182
- }
183
-
184
- /** Restores discarded tabs on demand without losing their urls; every discarded tab keeps its url for reload. */
185
- export function restorediscarded(tabs: tabshape[]): Array<{ tabid: number; url: string }> {
186
- return tabs.filter((tab) => tab.discarded && tab.url.length > 0).map((tab) => ({ tabid: tab.tabid, url: tab.url }));
187
- }
188
-
189
- /** Captures one tab layout with name, tabs, groups, positions and window bounds from the live browser state. */
190
- export function buildlayout(
191
- name: string,
192
- tabs: tabshape[],
193
- windows: windowshape[],
194
- groups: tabgrouprecord[],
195
- scratchwindowids: number[],
196
- at: number,
197
- ): tablayout {
198
- return {
199
- name,
200
- tabs: tabs.map((tab) => ({
201
- url: tab.url,
202
- title: tab.title,
203
- pinned: tab.pinned,
204
- index: tab.index,
205
- windowid: tab.windowid,
206
- })),
207
- groups: groups.map((group) => ({
208
- name: group.name,
209
- color: group.color,
210
- tabids: group.tabids.filter((tabid) => tabs.some((tab) => tab.tabid === tabid)),
211
- collapsed: group.collapsed,
212
- })),
213
- windows: windows.map((item) => ({
214
- windowid: item.windowid,
215
- state: {
216
- bounds: { left: item.left, top: item.top, width: item.width, height: item.height },
217
- maximized: item.state === "maximized",
218
- profile: item.incognito ? "incognito" : scratchwindowids.includes(item.windowid) ? "scratch" : "normal",
219
- },
220
- })),
221
- savedat: at,
222
- };
223
- }
224
-
225
- /** Plans the restore of one saved layout: only urls that are not already open come back, in layout order. */
226
- export function layoutrestoreplan(layout: tablayout, openurls: string[]): string[] {
227
- const open = new Set(openurls.map((url) => normalizedtaburl(url)));
228
- return layout.tabs.map((tab) => tab.url).filter((url) => url.length > 0 && !open.has(normalizedtaburl(url)));
229
- }
230
-
231
- /** Keeps tabgroup membership through moves: member ids survive, their order follows the live tab order and closed members drop out. */
232
- export function regroupaftermoves(groups: tabgrouprecord[], tabs: tabshape[], at: number): tabgrouprecord[] {
233
- const order = new Map(tabs.map((tab) => [tab.tabid, tab.index]));
234
- return groups.map((group) => {
235
- const members = group.tabids.filter((tabid) => order.has(tabid));
236
- if (members.length === 0) return group;
237
- const ordered = [...members].sort((left, right) => (order.get(left) ?? 0) - (order.get(right) ?? 0));
238
- return ordered.length === group.tabids.length && ordered.every((tabid, index) => tabid === group.tabids[index])
239
- ? group
240
- : { ...group, tabids: ordered, savedat: at };
241
- });
242
- }
243
-
244
- /** Renames one stored tab group while keeping its color choice, member tabs and collapse state. */
245
- export function renamegroup(groups: tabgrouprecord[], name: string, newname: string, at: number): tabgrouprecord[] {
246
- return groups.map((group) => (group.name === name ? { ...group, name: newname, savedat: at } : group));
247
- }
248
-
249
- /** Counts the task tabs that live inside one window so the close gate can demand review. */
250
- export function tasktabsinwindow(tabs: tabshape[], windowid: number, tasktabids: number[]): number {
251
- const tasks = new Set(tasktabids);
252
- return tabs.filter((tab) => tab.windowid === windowid && tasks.has(tab.tabid)).length;
253
- }
254
-
255
- /** Selects the tabs a reviewed closepattern may close; the session tab itself is always refused protection. */
256
- export function closeselection(
257
- query: tabquery,
258
- tabs: tabshape[],
259
- sessiontabid: number,
260
- ): { targets: tabshape[]; refused: tabshape[] } {
261
- const matches = querymatches(query, tabs);
262
- return {
263
- targets: matches.filter((tab) => tab.tabid !== sessiontabid),
264
- refused: matches.filter((tab) => tab.tabid === sessiontabid),
265
- };
266
- }
267
-
268
- /** Applies one reviewed zoom step with no code ceiling; a step never crosses zero, so it keeps the current zoom instead. */
269
- export function zoomstep(current: number, direction: "in" | "out", step: number): number {
270
- const next = direction === "in" ? current + step : current - step;
271
- return next > 0 ? Number(next.toFixed(4)) : current;
272
- }
273
-
274
- /** Resolves the neighbor tab index a switchtab step activates, wrapping at both ends of the window. */
275
- export function switchtarget(
276
- tabs: tabshape[],
277
- direction: "next" | "previous",
278
- currentindex: number,
279
- ): number | undefined {
280
- if (tabs.length === 0) return undefined;
281
- const offset = direction === "next" ? 1 : -1;
282
- return (currentindex + offset + tabs.length) % tabs.length;
283
- }
284
-
285
- /** Orders the quick switcher list by recency with filter keys; unseen tabs follow in live index order. */
286
- export function switcherlist(
287
- tabs: tabshape[],
288
- recency: Array<{ tabid: number; at: number }>,
289
- filter: string,
290
- ): tabshape[] {
291
- const needle = filter.trim().toLowerCase();
292
- const matches = needle
293
- ? tabs.filter((tab) => tab.title.toLowerCase().includes(needle) || tab.url.toLowerCase().includes(needle))
294
- : [...tabs];
295
- const lastrun = new Map(recency.map((entry) => [entry.tabid, entry.at]));
296
- return [...matches].sort((left, right) => {
297
- const leftat = lastrun.get(left.tabid) ?? -1;
298
- const rightat = lastrun.get(right.tabid) ?? -1;
299
- if (leftat !== rightat) return rightat - leftat;
300
- return left.index - right.index;
301
- });
302
- }
303
-
304
- /** Dispatches the tab events of one watchtab registration into the step result, honoring the reviewed event filters. */
305
- export function watchtabdispatch(events: tabwatchevent[], watchid: string, filters: string[]): tabwatchevent[] {
306
- const allowed = filters.length > 0 ? new Set(filters) : undefined;
307
- return events.filter((event) => event.watchid === watchid && (allowed === undefined || allowed.has(event.event)));
308
- }
309
-
310
- /** Computes the per task badge from the live progress state of the task. */
311
- export function badgefromprogress(completed: number, total: number): { label: string; done: boolean } {
312
- if (total <= 0) return { label: "idle", done: false };
313
- if (completed >= total) return { label: "done", done: true };
314
- return { label: `${completed}/${total}`, done: false };
315
- }
316
-
317
- /** Grades the concurrent task tab budget: a user configured ceiling refuses, an absent ceiling never refuses. */
318
- export function tasktabgauge(
319
- used: number,
320
- ceiling: number | undefined,
321
- ): { used: number; ceiling: number | undefined; over: boolean } {
322
- return { used, ceiling, over: ceiling !== undefined && used > ceiling };
323
- }
324
-
325
- /** True when a window profile inherits the session origin grants; incognito windows stay separated. */
326
- export function windowprofilegrants(profile: "normal" | "incognito" | "scratch"): boolean {
327
- return profile !== "incognito";
328
- }
329
-
330
- /** Assigns every task tab of the plan progress, used when tabmeta routing records a tab for the plan steps. */
331
- export function assigntasktabs(
332
- progress: planprogress | undefined,
333
- planid: string,
334
- tabids: number[],
335
- now: number,
336
- ): planprogress {
337
- let next = progress;
338
- for (const tabid of tabids) next = assigntasktab(next, planid, tabid, now);
339
- return next ?? { planid, completedsteps: [], tasktabs: [], updatedat: now };
340
- }
341
-
342
- /** Returns the task tabs tracked by one plan progress, used for window close review and badge refresh. */
343
- export function trackedtasktabs(progress: planprogress | undefined, planid: string): number[] {
344
- return tasktabs(progress, planid);
345
- }
346
-
347
- /* ── Merged from filescommand.ts: the 1.1.90 consolidation interns the correlated filescommand logic here, so no variation of the same file lives beside another. ── */
348
- import { checksum } from "./data.js";
349
-
350
- /**
351
- * Files, clipboard and downloads command logics for the background executors.
352
- * Every correlated rule for batch queue state transitions, filename conflicts, verification matching, mime interception with the deny default, netlog correlation and header redaction, clipboard entry hashing, quarantine paths and scan verdicts, capture naming counters and cleanup sweeps lives in this file.
353
- */
354
-
355
- /** Legal batch queue transitions between the download states. */
356
- const downloadtransitions: Record<downloadstate, downloadstate[]> = {
357
- queued: ["running", "complete", "failed"],
358
- running: ["paused", "complete", "failed"],
359
- paused: ["running", "failed"],
360
- complete: [],
361
- failed: [],
362
- };
363
-
364
- /** True when the batch queue allows the transition between two download states. */
365
- export function transitionallowed(from: downloadstate, to: downloadstate): boolean {
366
- return downloadtransitions[from].includes(to);
367
- }
368
-
369
- /** Applies one batch queue state transition with its path, bytes, checksum and browser download id evidence; impossible transitions leave the record untouched. */
370
- export function advancedownload(
371
- record: downloadrecord,
372
- state: downloadstate,
373
- at: number,
374
- evidence?: { path?: string; bytes?: number; checksum?: string; downloadid?: number },
375
- ): downloadrecord {
376
- if (!transitionallowed(record.state, state)) return record;
377
- return {
378
- ...record,
379
- state,
380
- ...(evidence?.path !== undefined
381
- ? { path: evidence.path }
382
- : record.path !== undefined
383
- ? { path: record.path }
384
- : {}),
385
- ...(evidence?.bytes !== undefined
386
- ? { bytes: evidence.bytes }
387
- : record.bytes !== undefined
388
- ? { bytes: record.bytes }
389
- : {}),
390
- ...(evidence?.checksum !== undefined
391
- ? { checksum: evidence.checksum }
392
- : record.checksum !== undefined
393
- ? { checksum: record.checksum }
394
- : {}),
395
- ...(evidence?.downloadid !== undefined
396
- ? { downloadid: evidence.downloadid }
397
- : record.downloadid !== undefined
398
- ? { downloadid: record.downloadid }
399
- : {}),
400
- updatedat: at,
401
- };
402
- }
403
-
404
- /** True when the batch queue may start another download under the user configured concurrent choice; an absent window never blocks. */
405
- export function concurrentwindow(running: number, ceiling: number | undefined): boolean {
406
- return ceiling === undefined || running < ceiling;
407
- }
408
-
409
- /** Resolves a filename conflict with a sequence suffix while keeping the extension intact. */
410
- export function conflictfree(filename: string, taken: string[]): string {
411
- if (!taken.includes(filename)) return filename;
412
- const dot = filename.lastIndexOf(".");
413
- const base = dot > 0 ? filename.slice(0, dot) : filename;
414
- const extension = dot > 0 ? filename.slice(dot) : "";
415
- let sequence = 2;
416
- while (taken.includes(`${base}-${sequence}${extension}`)) sequence += 1;
417
- return `${base}-${sequence}${extension}`;
418
- }
419
-
420
- /** Derives the download filename of one reviewed url from the spec filename rule or the url basename. */
421
- export function downloadfilename(url: string, rule: string | undefined): string {
422
- if (rule && rule.trim()) return rule.trim();
423
- let name = "";
424
- try {
425
- const parsed = new URL(url);
426
- name = decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() ?? parsed.hostname);
427
- } catch {
428
- name = url;
429
- }
430
- return name || "download";
431
- }
432
-
433
- /** Verifies one completed download against the reviewed size and checksum expectations. */
434
- export function verifybytes(
435
- record: downloadrecord,
436
- expected: { bytes?: number; checksum?: string },
437
- ): { ok: boolean; summary: string; matches: { state: boolean; size: boolean; checksum: boolean } } {
438
- const statematch = record.state === "complete";
439
- const sizematch = expected.bytes === undefined ? true : record.bytes === expected.bytes;
440
- const checksummatch = expected.checksum === undefined ? true : record.checksum === expected.checksum;
441
- const ok = statematch && sizematch && checksummatch;
442
- const parts = [`state ${record.state}${statematch ? " matches" : " does not match the completed expectation"}`];
443
- if (expected.bytes !== undefined)
444
- parts.push(`size ${record.bytes ?? "unknown"} of ${expected.bytes} bytes ${sizematch ? "matches" : "differs"}`);
445
- if (expected.checksum !== undefined)
446
- parts.push(
447
- `checksum ${record.checksum ?? "unknown"} ${checksummatch ? "matches" : "differs from"} the reviewed ${expected.checksum}`,
448
- );
449
- return {
450
- ok,
451
- summary: `${ok ? "Verified" : "Failed to verify"} the download of ${record.filename}: ${parts.join("; ")}.`,
452
- matches: { state: statematch, size: sizematch, checksum: checksummatch },
453
- };
454
- }
455
-
456
- /** Matches one reviewed mime pattern with a trailing * wildcard against a mime type. */
457
- function mimepatternmatches(pattern: string, mime: string): boolean {
458
- if (!pattern.endsWith("*")) return pattern === mime;
459
- return mime.startsWith(pattern.slice(0, -1));
460
- }
461
-
462
- /** True when the mime type passes the reviewed filter: exclude wins, then include, then the default for unlisted mime types. */
463
- export function mimeallowed(filter: mimefilter, mime: string): boolean {
464
- if (filter.exclude.some((pattern) => mimepatternmatches(pattern, mime))) return false;
465
- if (filter.include.some((pattern) => mimepatternmatches(pattern, mime))) return true;
466
- return filter.default === "allow";
467
- }
468
-
469
- /** Redacts every header value of a captured request so exported netlogs never leak them. */
470
- export function redactheaders(headers: Record<string, string>): Record<string, string> {
471
- return Object.fromEntries(Object.entries(headers).map(([name]) => [name, "[redacted]"]));
472
- }
473
-
474
- /** Builds one netlog record correlated with its step through the request id. */
475
- export function netlogentry(input: {
476
- url: string;
477
- method: string;
478
- status: number;
479
- timing: number;
480
- requestid: string;
481
- stepid: string;
482
- at: number;
483
- }): netlogrecord {
484
- return {
485
- url: input.url,
486
- method: input.method,
487
- status: input.status,
488
- timing: input.timing,
489
- requestid: input.requestid,
490
- stepid: input.stepid,
491
- at: input.at,
492
- };
493
- }
494
-
495
- /** Returns the netlog records of one step, oldest first, correlated through the request ids. */
496
- export function netlogforstep(records: netlogrecord[], stepid: string): netlogrecord[] {
497
- return records.filter((record) => record.stepid === stepid);
498
- }
499
-
500
- /** Builds one clipboard entry from the payload hash and length; the payload text itself never persists. */
501
- export function clipentryof(
502
- kind: clipentry["kind"],
503
- payload: { hash: string; length: number },
504
- origin: string,
505
- stepid: string,
506
- at: number,
507
- ): clipentry {
508
- return { kind, hash: payload.hash, length: payload.length, origin, stepid, at };
509
- }
510
-
511
- /** Hashes one clipboard payload so audits and memory carry the hash only. */
512
- export function cliphash(payload: string): string {
513
- return checksum(payload);
514
- }
515
-
516
- /** Routes a quarantined path outside the downloads folder until release; traversal segments and backslashes never enter the routed path so the quarantine folder never opens onto another folder. */
517
- export function quarantinedpath(filename: string): string {
518
- const routed = filename
519
- .replace(/\\/g, "/")
520
- .split("/")
521
- .filter((part) => part !== "" && part !== "." && part !== ".." && part.trim() !== "")
522
- .join("/");
523
- return `devthink-quarantine/${routed}`;
524
- }
525
-
526
- /** Builds one quarantine entry outside the downloads folder with a pending scan verdict. */
527
- export function newquarantine(id: string, filename: string, reason: string, at: number): quarantineentry {
528
- const path = quarantinedpath(filename);
529
- return { id, path, reason, scan: "pending", at, updatedat: at };
530
- }
531
-
532
- /** Applies one scan verdict to a quarantined entry; hook failures stay pending instead of releasing. */
533
- export function scanresult(entry: quarantineentry, verdict: scanverdict, at: number): quarantineentry {
534
- return { ...entry, scan: verdict, updatedat: at };
535
- }
536
-
537
- /** Maps one scanning hook response to its verdict; absent or malformed responses stay pending as hook failures. */
538
- export function scanverdictof(response: unknown): scanverdict {
539
- if (!response || typeof response !== "object") return "pending";
540
- const verdict = (response as Record<string, unknown>).verdict;
541
- if (verdict === "clean" || verdict === "flagged" || verdict === "error") return verdict;
542
- return "pending";
543
- }
544
-
545
- /** Stamps the release ref of one quarantined entry after the policy gate approved it. */
546
- export function released(entry: quarantineentry, ref: string, at: number): quarantineentry {
547
- return { ...entry, release: ref, updatedat: at };
548
- }
549
-
550
- /** Trims the leading and trailing dashes of one slug part with a single linear scan, because a polynomial dash expression over library input answers attacker paced backtracking. */
551
- function trimdashes(value: string): string {
552
- let start = 0;
553
- let end = value.length;
554
- while (start < end && value.charCodeAt(start) === 45) start += 1;
555
- while (end > start && value.charCodeAt(end - 1) === 45) end -= 1;
556
- return value.slice(start, end);
557
- }
558
-
559
- /** Normalizes one capture name part into a filename safe lowercase slug. */
560
- function capturepart(value: string): string {
561
- return trimdashes(value.replace(/[^a-z0-9-]+/gi, "-")).toLowerCase() || "capture";
562
- }
563
-
564
- /** Builds one consistent capture filename from its task, step and sequence parts. */
565
- export function capturefilename(name: capturename, extension: string): string {
566
- const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
567
- return `${capturepart(name.task)}-${capturepart(name.step)}-${name.sequence}.${safeextension}`;
568
- }
569
-
570
- /** Advances the capture naming counter of one step base and returns the stamped sequence. */
571
- export function advancecounter(
572
- counters: Record<string, number>,
573
- base: string,
574
- ): { sequence: number; counters: Record<string, number> } {
575
- const sequence = (counters[base] ?? 0) + 1;
576
- return { sequence, counters: { ...counters, [base]: sequence } };
577
- }
578
-
579
- /** Stamps consistent capture names for the steps of one task with per task counters. */
580
- export function capturenames(
581
- counters: Record<string, number>,
582
- task: string,
583
- steps: string[],
584
- extension: string,
585
- ): { names: string[]; counters: Record<string, number> } {
586
- let current = { ...counters };
587
- const names = steps.map((step) => {
588
- const advanced = advancecounter(current, step);
589
- current = advanced.counters;
590
- return capturefilename({ task, step, sequence: advanced.sequence }, extension);
591
- });
592
- return { names, counters: current };
593
- }
594
-
595
- /** Collects the artifact references of open review cards so the cleanup sweeper keeps them. */
596
- export function referencedartifacts(
597
- plan: { steps: Array<{ id: string; kind: string; value?: string }> } | undefined,
598
- completed: string[],
599
- ): string[] {
600
- if (!plan) return [];
601
- return plan.steps
602
- .filter((step) => step.kind === "attachfile" && !completed.includes(step.id))
603
- .map((step) => step.value ?? "")
604
- .filter((value) => value.trim().length > 0);
605
- }
606
-
607
- /** Plans one cleanup sweep by age and kind while keeping referenced artifacts and honoring the keep policies. */
608
- export function sweepplan(
609
- entries: artifactinventoryentry[],
610
- rules: cleanuprule[],
611
- now: number,
612
- keeprefs: string[],
613
- ): { remove: string[]; keep: string[] } {
614
- const remove = new Set<string>();
615
- for (const rule of rules) {
616
- const matching = entries.filter((entry) => rule.kind === "any" || entry.kind === rule.kind);
617
- const aged = matching.filter((entry) => now - entry.at >= rule.age);
618
- const kept: artifactinventoryentry[] = [];
619
- if (rule.keep === "all") kept.push(...aged);
620
- else if (rule.keep === "latest") {
621
- const newest = [...aged].sort((left, right) => right.at - left.at)[0];
622
- if (newest) kept.push(newest);
623
- }
624
- for (const entry of aged) {
625
- if (kept.some((item) => item.id === entry.id)) continue;
626
- if (keeprefs.includes(entry.id) || keeprefs.includes(entry.name)) continue;
627
- remove.add(entry.id);
628
- }
629
- }
630
- return { remove: [...remove], keep: entries.filter((entry) => !remove.has(entry.id)).map((entry) => entry.id) };
631
- }
632
-
633
- /** Reads the step ids a namecaptures step should stamp from its options or the plan steps. */
634
- export function capturesteps(options: Record<string, unknown>, plan: { steps: toolstep[] }): string[] {
635
- const listed = Array.isArray(options.steps)
636
- ? options.steps.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
637
- : [];
638
- return listed.length > 0 ? listed : plan.steps.map((step) => step.id);
639
- }
640
-
641
- /* ── Merged from navigation.ts: the 1.1.90 consolidation interns the correlated navigation logic here, so no variation of the same file lives beside another. ── */
642
- /**
643
- * Navigation intelligence logic of the 1.1.74 family.
644
- * Every correlated rule for the predicted prefetching of the next pages, the read only preconnection of expected origins, the deep link patterns of the common web apps, the closed tab reopening under a grant recheck, the navigation trail restoration from the urlhistory, the consent aware navigation pause with its pending queue, the per domain navigation rate windows, the clipboard url opening behind a user gesture, the url safety verification with lookalike heuristics and the curated batch opening with one tab per link lives in this file.
645
- * The navigation layer never bypasses a review: navintent and prefetchpage stay read only observations that issue no request on their own, prefetchpage never fires a mutating request while warming and drops its predictions the moment the plan they came from changes, preconnectorigin opens sockets that stay read only and revocable and honor the host grants of the session, deeplinkapp builds its urls only from reviewed parameters against the target origin grant, reopentab refuses a tab whose origin lost its grant, pausenavconsent freezes every navigation kind while a consent prompt is open and queues the pending navigation until the answer, navratelimit delays a full window instead of dropping the navigation silently with the window size and the ceiling staying user choices, openclipboardurl requires the origin grant before opening, checksafeurl refuses the hosts that imitate the granted origins, and batchopenlinks verifies every url before one tab opens while the batch size bound stays the user's choice with no hardcoded ceiling.
646
- */
647
-
648
- /** Reads the hostname of one url for the per domain navigation accounting; an unparsable url carries none. */
649
- function hostof(url: string): string {
650
- try {
651
- return new URL(url).hostname.toLowerCase();
652
- } catch {
653
- return "";
654
- }
655
- }
656
-
657
- /** Reads the origin of one url for the grant checks; an unparsable url carries none. */
658
- function originof(url: string): string {
659
- try {
660
- return new URL(url).origin;
661
- } catch {
662
- return "";
663
- }
664
- }
665
-
666
- /** Predicts the next urls of an approved plan: the navigation values of the steps are read in step order, the urlhistory of the run weighs every candidate by how often the run already visited it, and the predictions rank by the combined step order and history confidence so the reviewer reads exactly which pages the plan is about to need. */
667
- export function navintent(input: { id: string; steps: toolstep[]; visits?: urlvisit[]; now: number }): prefetchplan {
668
- if (input.id.trim() === "")
669
- throw new Error("The navintent prediction needs its id; every prefetch plan carries its identity.");
670
- const candidates: string[] = [];
671
- for (const step of input.steps) {
672
- const value = (step.value ?? "").trim();
673
- if (value === "" || !value.startsWith("https://")) continue;
674
- if (!candidates.includes(value)) candidates.push(value);
675
- }
676
- const visits = input.visits ?? [];
677
- const scored = candidates.map((url, index) => {
678
- const frequency = visits.filter((visit) => visit.url === url).length;
679
- const stepcomponent = candidates.length > 0 ? (candidates.length - index) / candidates.length : 0;
680
- const historycomponent = frequency / (frequency + 1);
681
- return { url, confidence: Math.round(((stepcomponent + historycomponent) / 2) * 1000) / 1000, order: index };
682
- });
683
- const predictedurls = scored
684
- .sort((one, two) => two.confidence - one.confidence || one.order - two.order)
685
- .map((entry) => ({ url: entry.url, confidence: entry.confidence }));
686
- return { id: input.id.trim(), predictedurls, createdat: input.now };
687
- }
688
-
689
- /** Warms the predicted next pages through speculative dns: the warming set keeps only the urls the session grants cover, a stored plan whose planid differs from the live plan drops every prediction it carried because stale predictions never warm a page the new plan no longer visits, and the module itself issues no request at all — the page world resolves the dns hints read only while no mutating request ever fires during the warming. */
690
- export function prefetchpage(input: {
691
- id: string;
692
- planid: string;
693
- urls: string[];
694
- grants: string[];
695
- stored?: prefetchplan;
696
- now: number;
697
- }): { plan: prefetchplan; allowed: string[]; refused: string[]; dropped: string[] } {
698
- if (input.id.trim() === "")
699
- throw new Error("The prefetch warming needs its id; every prefetch plan carries its identity.");
700
- if (input.planid.trim() === "")
701
- throw new Error("The prefetch warming names the plan it warms for; predictions without their plan drop.");
702
- const dropped: string[] =
703
- input.stored !== undefined && input.stored.planid !== undefined && input.stored.planid !== input.planid.trim()
704
- ? input.stored.predictedurls.map((entry) => entry.url)
705
- : [];
706
- const allowed: string[] = [];
707
- const refused: string[] = [];
708
- for (const url of input.urls) {
709
- const origin = originof(url);
710
- if (origin !== "" && input.grants.includes(origin)) {
711
- if (!allowed.includes(url)) allowed.push(url);
712
- continue; /* a granted duplicate warms once; a duplicate stays allowed and never lands in the refused list */
713
- }
714
- if (!refused.includes(url)) refused.push(url);
715
- }
716
- const plan: prefetchplan = {
717
- id: input.id.trim(),
718
- planid: input.planid.trim(),
719
- predictedurls: allowed.map((url) => ({
720
- url,
721
- confidence: input.stored?.predictedurls.find((entry) => entry.url === url)?.confidence ?? 0.5,
722
- })),
723
- createdat: input.now,
724
- };
725
- return { plan, allowed, refused, dropped };
726
- }
727
-
728
- /** Opens the read only sockets of the expected origins ahead of the steps that need them: only the origins the session grants cover may preconnect, every target carries the time its connection is expected, and every socket stays read only and revocable because a preconnect warms the transport and never carries a request of its own. */
729
- export function preconnectorigin(input: { origins: string[]; grants: string[]; now: number }): {
730
- targets: preconnecttarget[];
731
- refused: string[];
732
- } {
733
- const targets: preconnecttarget[] = [];
734
- const refused: string[] = [];
735
- for (const origin of input.origins) {
736
- const candidate = origin.trim();
737
- if (candidate === "") continue;
738
- if (!input.grants.includes(candidate)) {
739
- if (!refused.includes(candidate)) refused.push(candidate);
740
- continue;
741
- }
742
- if (targets.some((target) => target.origin === candidate)) continue;
743
- targets.push({ origin: candidate, expectedat: input.now, connected: true });
744
- }
745
- return { targets, refused };
746
- }
747
-
748
- /** The built in deep link patterns of the common web apps: every entry names its app, the origin its route builds into, the route template with its named parameters and the parameter names a reviewed step must supply. */
749
- const deeplinkcatalog: deeplinkpattern[] = [
750
- { app: "github", origin: "https://github.com", route: "/{owner}/{repo}", params: ["owner", "repo"] },
751
- { app: "github", origin: "https://github.com", route: "/{owner}", params: ["owner"] },
752
- { app: "youtube", origin: "https://www.youtube.com", route: "/watch?v={id}", params: ["id"] },
753
- { app: "youtube", origin: "https://www.youtube.com", route: "/results?search_query={search}", params: ["search"] },
754
- { app: "maps", origin: "https://www.google.com", route: "/maps/search/{query}", params: ["query"] },
755
- { app: "wikipedia", origin: "https://en.wikipedia.org", route: "/wiki/{title}", params: ["title"] },
756
- { app: "amazon", origin: "https://www.amazon.com", route: "/s?k={search}", params: ["search"] },
757
- { app: "x", origin: "https://x.com", route: "/{user}", params: ["user"] },
758
- ];
759
-
760
- /** Builds one deep link url of a common web app from its route pattern and the reviewed parameters: the app name resolves into its deeplinkpattern, the first pattern whose named parameters the reviewed step supplies wins, a missing parameter refuses loudly instead of building a half url, and every substituted value encodes so the route stays exactly what the pattern names. */
761
- export function deeplinkapp(input: { app: string; params: Record<string, string>; patterns?: deeplinkpattern[] }): {
762
- url: string;
763
- pattern: deeplinkpattern;
764
- } {
765
- const app = input.app.trim().toLowerCase();
766
- if (app === "")
767
- throw new Error("The deep link names its web app; the pattern grammar builds nothing without the app.");
768
- const patterns = [...(input.patterns ?? []), ...deeplinkcatalog].filter(
769
- (pattern) => pattern.app.trim().toLowerCase() === app,
770
- );
771
- if (patterns.length === 0)
772
- throw new Error(
773
- `The deep link pattern of ${input.app.trim()} is not a known web app; the reviewed step names an app the catalog carries.`,
774
- );
775
- for (const pattern of patterns) {
776
- const supplied = pattern.params.map((name) => input.params[name]);
777
- if (supplied.some((value) => value === undefined || value.trim() === "")) continue;
778
- let url = `${pattern.origin}${pattern.route}`;
779
- for (const [index, name] of pattern.params.entries())
780
- url = url.replaceAll(`{${name}}`, encodeURIComponent((supplied[index] as string).trim()));
781
- return { url, pattern };
782
- }
783
- throw new Error(
784
- `The deep link pattern of ${app} needs its reviewed parameters ${[...new Set(patterns.flatMap((pattern) => pattern.params))].join(", ")}; the pattern builds no half url.`,
785
- );
786
- }
787
-
788
- /** Restores one recently closed tab from its closedtabrecord: the most recent record the user closed comes back first, an explicit url restores its own record, a record whose origin lost its grant refuses loudly instead of silently reopening, and a reopened record never reopens twice while the retention window keeps it for the audit trail. */
789
- export function reopentab(input: {
790
- records: closedtabrecord[];
791
- grants: string[];
792
- url?: string;
793
- now: number;
794
- }): closedtabrecord {
795
- const ordered = [...input.records].sort((one, two) => two.closedat - one.closedat);
796
- if (input.url !== undefined && input.url.trim() !== "") {
797
- const wanted = input.url.trim();
798
- const record = ordered.find((entry) => entry.url === wanted);
799
- if (!record)
800
- throw new Error(
801
- `No closed tab record carries the url ${wanted}; the reopening restores a tab the retention window still remembers.`,
802
- );
803
- if (record.reopenedat !== undefined)
804
- throw new Error(
805
- `The closed tab record of ${wanted} was already reopened at ${new Date(record.reopenedat).toISOString()}; a reopened record never reopens twice.`,
806
- );
807
- if (!input.grants.includes(originof(record.url)))
808
- throw new Error(
809
- `The origin ${originof(record.url)} of the closed tab ${wanted} lost its grant; the reopening rechecks the consent and refuses.`,
810
- );
811
- return { ...record, reopenedat: input.now };
812
- }
813
- const fresh = ordered.filter((entry) => entry.reopenedat === undefined);
814
- if (fresh.length === 0) {
815
- if (ordered.length > 0)
816
- throw new Error("No unopened closed tab record is available to reopen; every remembered tab already came back.");
817
- throw new Error("No closed tab record is available to reopen; the retention window remembers none.");
818
- }
819
- const granted = fresh.find((entry) => input.grants.includes(originof(entry.url)));
820
- if (!granted)
821
- throw new Error(
822
- `The origin ${originof(fresh[0]?.url ?? "")} of the most recently closed tab lost its grant; the reopening rechecks the consent and refuses.`,
823
- );
824
- return { ...granted, reopenedat: input.now };
825
- }
826
-
827
- /** Rebuilds the navigation trail of one run from its urlhistory for the audit: the visits of the run order by their time, consecutive duplicates fold so the trail reads as the path the run walked, and repeated restores of the same history return the same entries so a replayed trail never doubles itself; the fresh tab replay stays an on demand operation of the executor because the trail itself stays a read only record. */
828
- export function restoretrail(input: { visits: urlvisit[]; runid?: string }): navtrailentry[] {
829
- const visits =
830
- input.runid !== undefined && input.runid.trim() !== ""
831
- ? input.visits.filter((visit) => visit.runid === input.runid)
832
- : input.visits;
833
- const ordered = [...visits].sort((one, two) => one.at - two.at || (one.url < two.url ? -1 : 1));
834
- const entries: navtrailentry[] = [];
835
- for (const visit of ordered) {
836
- if (entries.length > 0 && entries[entries.length - 1]?.url === visit.url) continue;
837
- entries.push({
838
- url: visit.url,
839
- at: visit.at,
840
- ...(input.runid !== undefined && input.runid.trim() !== "" ? { runid: input.runid } : {}),
841
- });
842
- }
843
- return entries;
844
- }
845
-
846
- /** Freezes navigation while a consent prompt is open: the pause carries its reason in plain language, a navigation that arrives while the freeze holds queues its url as the pending navigation of the record, and the queued navigation waits for the answer of the prompt instead of dropping silently. */
847
- export function pausenavconsent(input: {
848
- reason?: string;
849
- url?: string;
850
- stepid?: string;
851
- stored?: navpause;
852
- now: number;
853
- }): navpause {
854
- const pausedat = input.stored?.pausedat ?? input.now;
855
- const reason = input.stored?.reason ?? input.reason ?? "a consent prompt is open";
856
- const incomingurl = input.url === undefined ? "" : input.url;
857
- const incoming = incomingurl.trim() !== "";
858
- const pendingurl = incoming ? incomingurl.trim() : input.stored?.pendingurl;
859
- const pendingstepid = incoming
860
- ? input.stepid !== undefined && input.stepid.trim() !== ""
861
- ? input.stepid.trim()
862
- : undefined
863
- : input.stored?.pendingstepid;
864
- return {
865
- pausedat,
866
- reason,
867
- ...(pendingurl !== undefined ? { pendingurl } : {}),
868
- ...(pendingstepid !== undefined ? { pendingstepid } : {}),
869
- updatedat: input.now,
870
- };
871
- }
872
-
873
- /** Resumes the frozen navigation and hands the queued navigation back: the resume returns the pending url the pause held so the executor continues exactly where the consent prompt stopped it, and a resume without a stored pause changes nothing. */
874
- export function resumenavconsent(input: { stored?: navpause; now: number }): { resumed: boolean; queued?: navpause } {
875
- if (input.stored === undefined || input.stored.pausedat === undefined) return { resumed: false };
876
- return { resumed: true, queued: input.stored };
877
- }
878
-
879
- /** Tracks the navigation count of one domain inside the sliding window: every navigation stamps its hit into the window, the hits older than the user configured window size age out as the window slides forward, a full window delays the navigation for the milliseconds the oldest hit needs to age out and never drops it silently, and an absent window size or ceiling leaves the counting manual because both stay user choices with no code default. */
880
- export function navratelimit(input: {
881
- stored?: ratelimitwindow;
882
- url: string;
883
- now: number;
884
- window?: number;
885
- ceiling?: number;
886
- }): { window: ratelimitwindow; allowed: boolean; waitms: number; reason: string } {
887
- const domain = hostof(input.url);
888
- if (domain === "")
889
- throw new Error("The navigation rate window needs its url so the count lands on the right domain.");
890
- const size =
891
- input.window !== undefined && Number.isFinite(input.window) && input.window > 0
892
- ? input.window
893
- : input.stored?.window;
894
- const ceiling =
895
- input.ceiling !== undefined && Number.isInteger(input.ceiling) && input.ceiling >= 1
896
- ? input.ceiling
897
- : input.stored?.ceiling;
898
- const hits = (input.stored?.hits ?? [])
899
- .filter((hit) => (size === undefined ? hit <= input.now : input.now - hit < size))
900
- .sort((one, two) => one - two);
901
- const oldest = hits[0];
902
- const resetat = size !== undefined ? (oldest !== undefined ? oldest + size : input.now + size) : 0;
903
- if (size === undefined || ceiling === undefined) {
904
- const window: ratelimitwindow = {
905
- domain,
906
- count: hits.length + 1,
907
- resetat,
908
- ...(size !== undefined ? { hits: [...hits, input.now], window: size } : {}),
909
- ...(ceiling !== undefined ? { ceiling } : {}),
910
- };
911
- return {
912
- window,
913
- allowed: true,
914
- waitms: 0,
915
- reason: `The navigation of ${domain} counts at ${window.count}${ceiling !== undefined ? ` under the user ceiling of ${ceiling}` : " with no user ceiling"}; the sliding window stays a user choice with no code default.`,
916
- };
917
- }
918
- if (hits.length >= ceiling) {
919
- const waitms = Math.max(1, (oldest ?? input.now) + size - input.now);
920
- const window: ratelimitwindow = { domain, count: hits.length, resetat, hits, window: size, ceiling };
921
- return {
922
- window,
923
- allowed: false,
924
- waitms,
925
- reason: `The sliding window of ${domain} holds ${hits.length} of the ${ceiling} navigation${ceiling === 1 ? "" : "s"} the user allows per ${size} milliseconds; the navigation waits ${waitms} milliseconds for the oldest hit to age out and never drops silently.`,
926
- };
927
- }
928
- const window: ratelimitwindow = {
929
- domain,
930
- count: hits.length + 1,
931
- resetat,
932
- hits: [...hits, input.now],
933
- window: size,
934
- ceiling,
935
- };
936
- return {
937
- window,
938
- allowed: true,
939
- waitms: 0,
940
- reason: `The navigation of ${domain} counts against the sliding window: ${window.count} of ${ceiling} inside ${size} milliseconds.`,
941
- };
942
- }
943
-
944
- /** Reads one url from the clipboard text of an explicit user action: the text must parse into an HTTPS url, the origin of the url must sit inside the session grants before anything opens, and a clipboard that holds anything else refuses loudly instead of guessing. */
945
- export function openclipboardurl(input: { text: string; grants: string[] }): { url: string } {
946
- const text = input.text.trim();
947
- let parsed: URL;
948
- try {
949
- parsed = new URL(text);
950
- } catch {
951
- throw new Error("The clipboard holds no valid url; the opening refuses instead of guessing.");
952
- }
953
- if (parsed.protocol !== "https:")
954
- throw new Error("The clipboard url must use HTTPS; the opening refuses the weaker scheme.");
955
- const url = parsed.toString();
956
- if (!input.grants.includes(parsed.origin))
957
- throw new Error(
958
- `The origin ${parsed.origin} of the clipboard url sits outside the session grants; the opening requires the origin grant.`,
959
- );
960
- return { url };
961
- }
962
-
963
- /** Verifies one url for safety: the scheme must stay HTTPS, the url carries no embedded credentials, the host is no private network or raw address, and a host that imitates a granted origin — the granted host embedded in a longer host, joined through hyphens or squatting through punycode — refuses with the reason the ui shows before anything opens. */
964
- export function checksafeurl(input: { url: string; granted?: string[]; now: number }): safetyverdict {
965
- const reasons: string[] = [];
966
- let safe = true;
967
- let parsed: URL;
968
- try {
969
- parsed = new URL(input.url);
970
- } catch {
971
- return { url: input.url, safe: false, reasons: ["the url does not parse"], at: input.now };
972
- }
973
- if (parsed.protocol !== "https:") {
974
- safe = false;
975
- reasons.push("the url must use HTTPS");
976
- }
977
- if (parsed.username || parsed.password) {
978
- safe = false;
979
- reasons.push("the url carries embedded credentials");
980
- }
981
- const host = parsed.hostname.toLowerCase();
982
- const privatelist = ["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"];
983
- if (
984
- privatelist.includes(host) ||
985
- /^10\./.test(host) ||
986
- /^192\.168\./.test(host) ||
987
- /^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
988
- /^169\.254\./.test(host)
989
- ) {
990
- safe = false;
991
- reasons.push(`the host ${host} is a private network target`);
992
- }
993
- if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || /^\[?[0-9a-f:]+\]?$/i.test(host)) {
994
- safe = false;
995
- reasons.push(`the host ${host} is a raw address without a domain`);
996
- }
997
- if (host.split(".").some((label) => label.startsWith("xn--"))) {
998
- safe = false;
999
- reasons.push(`the host ${host} carries punycode labels a granted origin never uses`);
1000
- }
1001
- for (const granted of input.granted ?? []) {
1002
- let grantedhost = "";
1003
- try {
1004
- grantedhost = new URL(granted).hostname.toLowerCase();
1005
- } catch {
1006
- continue;
1007
- }
1008
- if (grantedhost === "" || host === grantedhost) continue;
1009
- if (host.endsWith(`.${grantedhost}`)) continue;
1010
- const grantedflat = grantedhost.split(".").join("").replaceAll("-", "");
1011
- const imitates =
1012
- host.includes(grantedhost) || (grantedflat !== "" && host.replaceAll("-", "").includes(grantedflat));
1013
- if (imitates) {
1014
- safe = false;
1015
- reasons.push(`the host ${host} imitates the granted origin ${grantedhost}`);
1016
- }
1017
- }
1018
- return { url: input.url, safe, reasons, at: input.now };
1019
- }
1020
-
1021
- /** Opens one curated list of links with one tab per link: every url verifies through checksafeurl before anything opens and one unsafe url refuses the whole batch as a curated record that waits for review, the batch size never passes the user configured ceiling because the bound stays a user choice only, and the per domain sliding windows account every open across the whole batch while a full window moves its url into the waits the caller delays on and never drops silently. */
1022
- export function batchopenlinks(input: {
1023
- id: string;
1024
- urls: string[];
1025
- grants: string[];
1026
- windows: ratelimitwindow[];
1027
- now: number;
1028
- window?: number;
1029
- sizelimit?: number;
1030
- }): {
1031
- batch: linkbatch;
1032
- open: Array<{ url: string; verdict: safetyverdict }>;
1033
- refused: Array<{ url: string; reasons: string[] }>;
1034
- ordered: string[];
1035
- waits: Array<{ url: string; waitms: number }>;
1036
- windows: ratelimitwindow[];
1037
- reason: string;
1038
- } {
1039
- if (input.id.trim() === "") throw new Error("The link batch needs its id; every curated batch carries its identity.");
1040
- if (
1041
- input.sizelimit !== undefined &&
1042
- Number.isInteger(input.sizelimit) &&
1043
- input.sizelimit >= 1 &&
1044
- input.urls.length > input.sizelimit
1045
- )
1046
- throw new Error(
1047
- `The batch of ${input.urls.length} url${input.urls.length === 1 ? "" : "s"} passes the user configured ceiling of ${input.sizelimit}; the user raises the ceiling or trims the batch because the bound stays a user choice only.`,
1048
- );
1049
- const batch: linkbatch = { id: input.id.trim(), urls: [...input.urls], grants: [...input.grants], at: input.now };
1050
- const verdicts = input.urls.map((url) => ({
1051
- url,
1052
- verdict: checksafeurl({ url, granted: input.grants, now: input.now }),
1053
- }));
1054
- const refused = verdicts
1055
- .filter((entry) => !entry.verdict.safe)
1056
- .map((entry) => ({ url: entry.url, reasons: entry.verdict.reasons }));
1057
- if (refused.length > 0) {
1058
- return {
1059
- batch,
1060
- open: [],
1061
- refused,
1062
- ordered: [],
1063
- waits: [],
1064
- windows: input.windows,
1065
- reason: `The batch refuses as a whole because ${refused.length} of ${input.urls.length} url${refused.length === 1 ? " is" : "s are"} unsafe; the curated list waits for review before one tab opens.`,
1066
- };
1067
- }
1068
- const open: Array<{ url: string; verdict: safetyverdict }> = [];
1069
- const waits: Array<{ url: string; waitms: number }> = [];
1070
- const windows = [...input.windows];
1071
- for (const entry of verdicts) {
1072
- const domain = hostof(entry.url);
1073
- const storedwindow = windows.find((window) => window.domain === domain);
1074
- const decision = navratelimit({
1075
- ...(storedwindow !== undefined ? { stored: storedwindow } : {}),
1076
- url: entry.url,
1077
- now: input.now,
1078
- ...(input.window !== undefined ? { window: input.window } : {}),
1079
- });
1080
- if (!decision.allowed) {
1081
- waits.push({ url: entry.url, waitms: decision.waitms });
1082
- continue;
1083
- }
1084
- const index = windows.findIndex((window) => window.domain === domain);
1085
- if (index === -1) windows.push(decision.window);
1086
- else windows[index] = decision.window;
1087
- open.push(entry);
1088
- }
1089
- return {
1090
- batch: { ...batch, reviewedat: input.now },
1091
- open,
1092
- refused: [],
1093
- ordered: open.map((entry) => entry.url),
1094
- waits,
1095
- windows,
1096
- reason:
1097
- waits.length > 0
1098
- ? `The batch opens ${open.length} curated url${open.length === 1 ? "" : "s"} with one tab per link while ${waits.length} url${waits.length === 1 ? " waits" : "s wait"} on a full rate window and never drops silently.`
1099
- : `The batch opens ${open.length} curated url${open.length === 1 ? "" : "s"} with one tab per link after every url passed its safety check.`,
1100
- };
1101
- }