@wenathlan/devthink 2.0.13 → 2.0.15

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 +27 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/core.ts ADDED
@@ -0,0 +1,1473 @@
1
+ /**
2
+ * @file core.ts — generic pure utilities shared by every domain module.
3
+ *
4
+ * Third-person observer view — library-first, root-first, production-ready.
5
+ *
6
+ * v2.1.15 Phase A consolidation: this module is now a TRUE leaf of the
7
+ * dependency DAG (imports only constants.js below it plus node:* builtins).
8
+ * Every legacy compact duplicate that used to live here — the short-named
9
+ * request builders (transformRequest, buildAntigravity, buildGeminiCLI,
10
+ * fetchCascade), the SSE transformers (parseSSEChunk, normalizePayloads,
11
+ * transformToOpenAI, transformToAnthropic), the model classifiers (isCLIOnly,
12
+ * isImageModel, resolveModelId, parseVariant, BUDGET_MAP, mapBudget,
13
+ * endpointsForModel), the identity helpers (ua, apiClient, fingerprint), the
14
+ * schema cleaner (cleanSchema, ALLOWED), the header stripper (FORBIDDEN,
15
+ * stripForbidden), the id generators (genId, sessId, upId), sanitizeToolName,
16
+ * normalizeRole, TOOL_REGEX and withAutoRecovery — was a dead duplicate of the
17
+ * canonical implementation in its domain owner and was deleted.
18
+ *
19
+ * v2.1.15 Phase B consolidation: this module absorbed the generic utility
20
+ * families that used to be duplicated across the tree — fnv1a64 (fingerprint
21
+ * hex-string lineage + the request bigint lineage, now accurately split into
22
+ * fnv1a64 / fnv1a64Bytes), getJitter (constants, plugin), getJitterMs
23
+ * (fingerprint, models), pickRandom (constants, fingerprint), isRetryable
24
+ * (auth, plugin, project), isRetryableStatus (models, request, search) and
25
+ * fetchWithTimeout (models, plugin, project, quota, search, version, auth's
26
+ * fetchTimeout). Every other module imports these from core.js.
27
+ */
28
+ import { randomInt } from "node:crypto";
29
+ import { FETCH_TIMEOUT_MS, JITTER_MAX_MS, JITTER_MIN_MS } from "./constants.js";
30
+
31
+ /**
32
+ * FNV-1a 32-bit hash over a UTF-16 string (per code unit), unsigned.
33
+ * Canonical implementation of the fnv1a32 family — every other module
34
+ * imports this instead of redefining it.
35
+ */
36
+ export const fnv1a32 = (s: string): number => {
37
+ let h = 0x811c9dc5;
38
+ for (let i = 0; i < s.length; i++) {
39
+ h ^= s.charCodeAt(i);
40
+ h = Math.imul(h, 0x01000193) >>> 0;
41
+ }
42
+ return h >>> 0;
43
+ };
44
+
45
+ /**
46
+ * Hex form of {@link fnv1a32}, zero-padded to 8 characters.
47
+ */
48
+ export const fnv1a32Hex = (s: string): string => fnv1a32(s).toString(16).padStart(8, "0");
49
+
50
+ /**
51
+ * FNV-1a 64-bit hash over UTF-16 code units, returned as a 16-char
52
+ * zero-padded lowercase hex string (fingerprint lineage — canonical of the
53
+ * fnv1a64 family). Uses the FNV-1a 64 offset basis 14695981039346656037 and
54
+ * prime 1099511628211 as inline literals; the 32-bit FNV constants live in
55
+ * constants.ts but the 64-bit pair is only used here, so it stays local and
56
+ * documented rather than imported.
57
+ */
58
+ export function fnv1a64(input: string): string {
59
+ let hash = 14695981039346656037n;
60
+ const prime = 1099511628211n;
61
+ for (let i = 0; i < input.length; i++) {
62
+ hash ^= BigInt(input.charCodeAt(i));
63
+ hash = (hash * prime) & 0xffffffffffffffffn;
64
+ }
65
+ return hash.toString(16).padStart(16, "0");
66
+ }
67
+
68
+ /**
69
+ * FNV-1a 64-bit hash over UTF-8 BYTES, returned as a bigint (request.ts
70
+ * lineage — the long deterministic-id digest). Distinct from
71
+ * {@link fnv1a64}: this variant hashes bytes (Buffer.from(input, "utf8"))
72
+ * instead of UTF-16 code units, so the two produce different digests for
73
+ * non-ASCII input and different return types; the accurate split keeps both
74
+ * historical behaviors available under honest names.
75
+ */
76
+ export function fnv1a64Bytes(input: string): bigint {
77
+ let hash = 14695981039346656037n;
78
+ const prime = 1099511628211n;
79
+ const buf = Buffer.from(input, "utf8");
80
+ for (let i = 0; i < buf.length; i++) {
81
+ hash ^= BigInt(buf[i]!);
82
+ hash = (hash * prime) & 0xffffffffffffffffn;
83
+ }
84
+ return hash;
85
+ }
86
+
87
+ /**
88
+ * Uniform jitter in [JITTER_MIN_MS, JITTER_MAX_MS] (0-80 ms) for cascade
89
+ * backoff randomization. Prefers the crypto-strong randomInt and falls back
90
+ * to Math.random. Same value distribution as {@link getJitter}.
91
+ */
92
+ export const jitter = (): number => {
93
+ try {
94
+ return randomInt(JITTER_MIN_MS, JITTER_MAX_MS + 1);
95
+ } catch {
96
+ return Math.floor(Math.random() * (JITTER_MAX_MS - JITTER_MIN_MS + 1)) + JITTER_MIN_MS;
97
+ }
98
+ };
99
+
100
+ /**
101
+ * Jitter draw in [JITTER_MIN_MS, JITTER_MAX_MS] (0-80 ms) — the historical
102
+ * getJitter name used by the cascade/retry loops. Identical distribution to
103
+ * {@link jitter}; both names are kept because both are published.
104
+ */
105
+ export function getJitter(): number {
106
+ return jitter();
107
+ }
108
+
109
+ /**
110
+ * Bounded jitter value in the inclusive [minMs, maxMs] range (defaults
111
+ * 0-80 ms). Crypto-strong randomInt with a Math.random fallback; used for
112
+ * Retry-After throttling and anti-rate-limit delays.
113
+ */
114
+ export function getJitterMs(minMs = 0, maxMs = 80): number {
115
+ try {
116
+ return randomInt(minMs, maxMs + 1);
117
+ } catch {
118
+ return Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Picks a uniformly random element from a readonly pool. Crypto-strong
124
+ * randomInt with a Math.random fallback (canonical of the pickRandom family;
125
+ * the former constants.ts Math.random variant and the fingerprint.ts
126
+ * secureRandom variant merge here — same distribution, stronger draw).
127
+ */
128
+ export function pickRandom<T>(pool: readonly T[]): T {
129
+ try {
130
+ return pool[randomInt(0, pool.length)]!;
131
+ } catch {
132
+ return pool[Math.floor(Math.random() * pool.length)]!;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Retry classification for the auth/project-discovery flows:
138
+ * 403, 404, 408, 429 and every 5xx are retryable. Canonical of the
139
+ * isRetryable family (auth.ts and project.ts lineages; the plugin.ts
140
+ * 3-condition variant had zero callers and was dropped).
141
+ */
142
+ export function isRetryable(status: number): boolean {
143
+ return status === 403 || status === 404 || status === 408 || status === 429 || status >= 500;
144
+ }
145
+
146
+ /**
147
+ * Retry classification for the endpoint-cascade flows: 403 (project IAM
148
+ * mismatch), 404 (rollout), 429 (rate limit) and the whole 5xx range.
149
+ * Canonical of the isRetryableStatus family — the live-validated request.ts
150
+ * cascade variant (408 is intentionally NOT retryable here; streaming.ts
151
+ * keeps its own list-driven transient variant under an accurate name).
152
+ */
153
+ export function isRetryableStatus(status: number): boolean {
154
+ if (!Number.isFinite(status)) return false;
155
+ return status === 429 || status === 403 || status === 404 || (status >= 500 && status <= 599);
156
+ }
157
+
158
+ /**
159
+ * Fetch with an abort-controller timeout and external-signal merge. Canonical
160
+ * of the fetchWithTimeout family (supersedes the auth.ts fetchTimeout alias:
161
+ * same positional signature and same 10s default). Behavior notes:
162
+ * - timeout default FETCH_TIMEOUT_MS (10s); pass the third argument to
163
+ * override (quota callers pass QUOTA_TIMEOUT_MS, version callers the 5s
164
+ * VERSION_FETCH_TIMEOUT_MS);
165
+ * - an init.signal is merged with the timer: either aborting aborts the
166
+ * fetch (an already-aborted external signal aborts immediately);
167
+ * - a `timeoutMs` property inside init (the models.ts lineage call style)
168
+ * takes precedence over the positional default;
169
+ * - AbortError propagates unwrapped — consumers that historically mapped it
170
+ * to a domain error (ProjectDiscoveryError 408 in project.ts, the plain
171
+ * timeout Errors in plugin/quota) wrap THIS call at their call sites.
172
+ */
173
+ export async function fetchWithTimeout(
174
+ url: string,
175
+ init: RequestInit = {},
176
+ timeoutMs: number = FETCH_TIMEOUT_MS,
177
+ ): Promise<Response> {
178
+ const timeout = (init as RequestInit & { timeoutMs?: number }).timeoutMs ?? timeoutMs;
179
+ const controller = new AbortController();
180
+ const timer = setTimeout(() => controller.abort(), timeout);
181
+
182
+ const external = init.signal as AbortSignal | undefined | null;
183
+ if (external) {
184
+ if (external.aborted) controller.abort();
185
+ else external.addEventListener("abort", () => controller.abort(), { once: true });
186
+ }
187
+
188
+ try {
189
+ const { timeoutMs: _t, signal: _s, ...rest } = init as Record<string, unknown>;
190
+ return await fetch(url, { ...rest, signal: controller.signal } as RequestInit);
191
+ } finally {
192
+ clearTimeout(timer);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * LRU map with O(1) re-insertion refresh; evicts the least-recently-used
198
+ * entry once the capacity is exceeded.
199
+ */
200
+ class LRU<K, V> extends Map<K, V> {
201
+ constructor(private max = 100) {
202
+ super();
203
+ }
204
+
205
+ override set(k: K, v: V) {
206
+ if (this.has(k)) this.delete(k);
207
+ super.set(k, v);
208
+ if (this.size > this.max) {
209
+ const first = this.keys().next().value;
210
+ if (first !== undefined) this.delete(first);
211
+ }
212
+ return this;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Shared LRU(100) cache keyed by session/model for thinking signatures.
218
+ * The single shared instance consumed by streaming.ts.
219
+ */
220
+ export const thinkingCache = new LRU<string, string>(100);
221
+
222
+ /* ── Merged: the grand merge section ── the correlated debug logging logics of the merged repository interned here, one surface without duplicate variations. ── */
223
+ import * as fs from "node:fs";
224
+ import * as os from "node:os";
225
+ import * as path from "node:path";
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Governance constants — identical values owned by constants.ts
229
+ // ---------------------------------------------------------------------------
230
+
231
+ import { DEBUG_BUFFER_TUI_LINES, DEBUG_LOG_MAX_BYTES, DEBUG_LOG_RETENTION_DAYS } from "./constants.js";
232
+ import { cfgDir } from "./config.js";
233
+ export { DEBUG_BUFFER_TUI_LINES, DEBUG_LOG_MAX_BYTES, DEBUG_LOG_RETENTION_DAYS };
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Section: Gemini CLI bypass context (documentation constant)
237
+ // ---------------------------------------------------------------------------
238
+
239
+ // v2.1.16 dead-code cleanup: the GEMINI_CLI_CONTEXT documentation aggregate
240
+ // had zero consumers repo-wide and was deleted. It carried a verbatim GOCSPX
241
+ // client-secret fragment and phantom model ids (gemini-2.0-flash-thinking-exp,
242
+ // gemini-antigravity-preview) that exist in no owner module; the real values
243
+ // live in constants.js (oauth pair, user agent, metadata, base url,
244
+ // PROJECT_FALLBACK) and models.js (model identification).
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Section: Governance constants (filesystem layout and limits)
248
+ // ---------------------------------------------------------------------------
249
+ // DEBUG_LOG_MAX_BYTES (10MB), DEBUG_LOG_RETENTION_DAYS (7) and
250
+ // DEBUG_BUFFER_TUI_LINES (1000) are imported from constants.js (identical
251
+ // values) and re-exported above.
252
+
253
+ /** Relative log dir from $HOME: ~/.config/opencode/antigravity-logs/ */
254
+ export const DEBUG_LOG_DIR_SUBPATH = ".config/opencode/antigravity-logs";
255
+
256
+ /** Active log filename */
257
+ export const DEBUG_LOG_FILE_NAME = "antigravity.log";
258
+
259
+ // Internal aliases kept from the legacy variant for readability below.
260
+ const PLUGIN_DIR_NAME = "opencode";
261
+ const LOG_SUBDIR = "antigravity-logs";
262
+ const LOG_FILE_NAME = DEBUG_LOG_FILE_NAME;
263
+ const MAX_FILE_SIZE_BYTES = DEBUG_LOG_MAX_BYTES;
264
+ const MAX_FILE_AGE_MS = DEBUG_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
265
+ const TUI_BUFFER_CAPACITY = DEBUG_BUFFER_TUI_LINES;
266
+
267
+ /** Cleanup trigger: check for expired logs every N writes */
268
+ const CLEANUP_EVERY_WRITES = 50;
269
+
270
+ function getDefaultLogDir(): string {
271
+ // Same result as path.join(home, ".config", "opencode", "antigravity-logs"),
272
+ // derived from the exported governance constant.
273
+ return path.join(os.homedir(), ...DEBUG_LOG_DIR_SUBPATH.split("/"));
274
+ }
275
+
276
+ function getLogFilePath(logDir: string): string {
277
+ return path.join(logDir, LOG_FILE_NAME);
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Section: Log levels (reconciled enum)
282
+ // -----------------------------------------------------------------------------
283
+ /**
284
+ * Reconciliation note: two numeric scales existed (0/1/2/3/99 legacy and
285
+ * 10/20/30/40/100 observer). The legacy compact scale is canonical because it
286
+ * is the merge base; environment parsing accepts BOTH numeric scales (see
287
+ * {@link parseAntigravityDebugEnv}).
288
+ */
289
+ export type LogLevelName = "debug" | "info" | "warn" | "error" | "silent";
290
+
291
+ export enum LogLevel {
292
+ DEBUG = 0,
293
+ INFO = 1,
294
+ WARN = 2,
295
+ ERROR = 3,
296
+ SILENT = 99,
297
+ }
298
+
299
+ const LEVEL_NAME_TO_ENUM: Record<string, LogLevel> = {
300
+ debug: LogLevel.DEBUG,
301
+ info: LogLevel.INFO,
302
+ warn: LogLevel.WARN,
303
+ warning: LogLevel.WARN, // compat alias accepted from the observer variant
304
+ error: LogLevel.ERROR,
305
+ silent: LogLevel.SILENT,
306
+ };
307
+
308
+ const LEVEL_ENUM_TO_NAME: Record<LogLevel, string> = {
309
+ [LogLevel.DEBUG]: "DEBUG",
310
+ [LogLevel.INFO]: "INFO",
311
+ [LogLevel.WARN]: "WARN",
312
+ [LogLevel.ERROR]: "ERROR",
313
+ [LogLevel.SILENT]: "SILENT",
314
+ };
315
+
316
+ /** Returns the first defined value among the given env variable names. */
317
+ function readFirstEnv(env: NodeJS.ProcessEnv, names: readonly string[]): string | undefined {
318
+ for (const name of names) {
319
+ const value = env[name];
320
+ if (value !== undefined) return value;
321
+ }
322
+ return undefined;
323
+ }
324
+
325
+ function parseBooleanEnv(value: string | undefined, defaultValue: boolean): boolean {
326
+ if (value === undefined) return defaultValue;
327
+ const v = value.trim().toLowerCase();
328
+ if (["1", "true", "yes", "on", "enabled"].includes(v)) return true;
329
+ if (["0", "false", "no", "off", "disabled"].includes(v)) return false;
330
+ return defaultValue;
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Section: Environment parsing (ANTIGRAVITY_DEBUG / OPENCODE_ANTIGRAVITY_DEBUG)
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export interface EnvParseResult {
338
+ /** true = logging active, false = completely silenced */
339
+ enabled: boolean;
340
+ /** Minimum level that will be emitted */
341
+ level: LogLevel;
342
+ /** Raw env string kept for debugging/introspection */
343
+ raw: string | undefined;
344
+ }
345
+
346
+ /** Tokens meaning "logger fully off". Matched against trimmed lowercase input. */
347
+ const DISABLED_TOKENS = new Set(["0", "false", "off", "disabled", "no", "none", ""]);
348
+
349
+ /** Tokens meaning "full debug enable" (most verbose level). */
350
+ const DEBUG_ENABLE_TOKENS = new Set(["1", "true", "yes", "on", "*", "all", "debug", "verbose", "trace", "silly"]);
351
+
352
+ /**
353
+ * Parses the debug env value (ANTIGRAVITY_DEBUG or
354
+ * OPENCODE_ANTIGRAVITY_DEBUG) into an enabled flag plus minimum level.
355
+ *
356
+ * Rules (merged from both variants):
357
+ * - undefined => enabled true, level WARN (observer default)
358
+ * - disabled tokens => enabled false, SILENT (0/false/off/disabled/no/none/"")
359
+ * - enable tokens => DEBUG (1/true/yes/on/star/all/debug/verbose/trace/silly)
360
+ * - numeric values => legacy scale 0..3 maps directly onto LogLevel;
361
+ * observer scale 10..40 maps DEBUG/INFO/WARN/ERROR;
362
+ * values >= 99 map to SILENT (legacy sentinel);
363
+ * anything else falls back to WARN
364
+ * - comma/semicolon/space/pipe separated lists pick the most verbose level
365
+ * mentioned (substring matching included, wildcards force DEBUG)
366
+ * - any other non-empty string defaults to DEBUG (user explicitly opted in)
367
+ *
368
+ * @param raw - raw env value
369
+ */
370
+ export function parseAntigravityDebugEnv(raw: string | undefined): EnvParseResult {
371
+ if (raw === undefined) {
372
+ return { enabled: true, level: LogLevel.WARN, raw };
373
+ }
374
+ const trimmed = String(raw).trim();
375
+ const lower = trimmed.toLowerCase();
376
+
377
+ if (DISABLED_TOKENS.has(lower)) {
378
+ // Explicit empty string counts as disabled when set; undefined stays default WARN.
379
+ return { enabled: false, level: LogLevel.SILENT, raw: trimmed };
380
+ }
381
+
382
+ if (DEBUG_ENABLE_TOKENS.has(lower)) {
383
+ return { enabled: true, level: LogLevel.DEBUG, raw: trimmed };
384
+ }
385
+
386
+ // Numeric quick path accepting both reconciled scales.
387
+ if (/^\d+$/.test(lower)) {
388
+ const n = Number(lower);
389
+ if (n >= 99) return { enabled: false, level: LogLevel.SILENT, raw: trimmed };
390
+ if (n <= 3) return { enabled: true, level: n as LogLevel, raw: trimmed }; // legacy direct cast
391
+ if (n <= 10) return { enabled: true, level: LogLevel.DEBUG, raw: trimmed };
392
+ if (n <= 20) return { enabled: true, level: LogLevel.INFO, raw: trimmed };
393
+ if (n <= 30) return { enabled: true, level: LogLevel.WARN, raw: trimmed };
394
+ if (n <= 40) return { enabled: true, level: LogLevel.ERROR, raw: trimmed };
395
+ return { enabled: true, level: LogLevel.WARN, raw: trimmed };
396
+ }
397
+
398
+ // Multi-token list: choose the most verbose (lowest numeric) level mentioned.
399
+ const tokens = lower
400
+ .split(/[,;\s|]+/g)
401
+ .map((s) => s.trim())
402
+ .filter(Boolean);
403
+
404
+ let minLevel: LogLevel | null = null;
405
+ for (const tok of tokens) {
406
+ if (tok in LEVEL_NAME_TO_ENUM) {
407
+ const lvl = LEVEL_NAME_TO_ENUM[tok];
408
+ if (lvl !== undefined && (minLevel === null || lvl < minLevel)) minLevel = lvl;
409
+ continue;
410
+ }
411
+ for (const [name, lvl] of Object.entries(LEVEL_NAME_TO_ENUM)) {
412
+ if (tok.includes(name)) {
413
+ if (minLevel === null || lvl < minLevel) minLevel = lvl;
414
+ }
415
+ }
416
+ if (tok.includes("*")) {
417
+ if (minLevel === null || LogLevel.DEBUG < minLevel) minLevel = LogLevel.DEBUG;
418
+ }
419
+ }
420
+
421
+ if (minLevel !== null) {
422
+ return { enabled: true, level: minLevel, raw: trimmed };
423
+ }
424
+
425
+ // Non-empty unknown string => DEBUG (user explicitly wanted logging).
426
+ if (trimmed.length > 0) {
427
+ return { enabled: true, level: LogLevel.DEBUG, raw: trimmed };
428
+ }
429
+
430
+ return { enabled: true, level: LogLevel.WARN, raw: trimmed };
431
+ }
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // Section: Secret redaction
435
+ // ---------------------------------------------------------------------------
436
+
437
+ /**
438
+ * Legacy pattern list: covers GOCSPX client secrets, OAuth client IDs,
439
+ * Bearer tokens, generic OAuth fields, query-string tokens, JWT-like
440
+ * triples and X-Goog headers.
441
+ */
442
+ const SECRET_PATTERNS: Array<{ regex: RegExp; replacement: string }> = [
443
+ // Full GOCSPX client secret
444
+ { regex: /GOCSPX-[A-Za-z0-9_\-]{20,}/g, replacement: "GOCSPX-***REDACTED***" },
445
+ // Client ID patterns (avoid leaking the full identifier in logs)
446
+ { regex: /681255809395-[a-z0-9]{20,}\.apps\.googleusercontent\.com/gi, replacement: "***CLIENT_ID_REDACTED***" },
447
+ { regex: /\b681255809395-oo8f…b135j\b/g, replacement: "***CLIENT_ID_REDACTED***" },
448
+
449
+ // Bearer tokens - header form or bare value
450
+ { regex: /(Bearer\s+)[A-Za-z0-9\-_\.=]+/gi, replacement: "$1***REDACTED***" },
451
+ { regex: /(Authorization['"]?\s*[:=]\s*['"]?)(Bearer\s+)?[A-Za-z0-9\-_\.]+/gi, replacement: "$1***REDACTED***" },
452
+
453
+ // Generic OAuth fields
454
+ {
455
+ regex:
456
+ /("?(?:access_token|refresh_token|id_token|client_secret|api_key|apikey|idToken|accessToken|refreshToken)"?\s*[:=]\s*["']?)([^"'\s,}\]]+)/gi,
457
+ replacement: "$1***REDACTED***",
458
+ },
459
+
460
+ // access_token as query parameter
461
+ { regex: /([?&](?:access_token|id_token|refresh_token|api_key|key)=)([^&\s]+)/gi, replacement: "$1***REDACTED***" },
462
+
463
+ // JWT-like tokens (three base64 parts)
464
+ { regex: /\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b/g, replacement: "***JWT_REDACTED***" },
465
+
466
+ // Google OAuth access tokens (ya29.*)
467
+ { regex: /\b(ya29\.[A-Za-z0-9_\-]+)\b/g, replacement: "ya29.***REDACTED***" },
468
+
469
+ // X-Goog headers carrying tokens
470
+ { regex: /(X-Goog-[A-Za-z\-]+:\s*)(.+)/gi, replacement: "$1***REDACTED***" },
471
+ ];
472
+
473
+ /**
474
+ * Redacts known secrets inside free-form text using the legacy pattern list.
475
+ * @param input - raw log line or message
476
+ */
477
+ export function redactSecretsPatterns(input: string): string {
478
+ let out = input;
479
+ for (const { regex, replacement } of SECRET_PATTERNS) {
480
+ // Reset lastIndex so globally reused regexes stay safe inside loops.
481
+ regex.lastIndex = 0;
482
+ out = out.replace(regex, replacement);
483
+ }
484
+ return out;
485
+ }
486
+
487
+ /**
488
+ * Text redactor from the observer variant. Complements {@link redactSecretsPatterns}
489
+ * with JSON/query key=value captures, long Bearer tokens, ya29.* Google OAuth
490
+ * access tokens and AIza* Google API keys.
491
+ * @param input - raw log line or message
492
+ */
493
+ export function redactSecretsText(input: string): string {
494
+ if (!input || typeof input !== "string") return input as unknown as string;
495
+ let out = input;
496
+
497
+ // 1. JSON / query style separators - capture key+separator, replace value.
498
+ // Value must be at least 8 chars to avoid false positives.
499
+ out = out.replace(
500
+ /(["']?(?:refresh_token|access_token|id_token|api_key|api[_-]?key|client_secret|client_id)["']?\s*[:=]\s*["']?)([^"'\s,;}\]&]{8,})/gi,
501
+ (_m, p1: string) => `${p1}[REDACTED]`,
502
+ );
503
+
504
+ // 2. Bearer header with long token
505
+ out = out.replace(/(Bearer\s+)[A-Za-z0-9\-_\.=]{20,}/gi, "$1[REDACTED]");
506
+
507
+ // 3. Google OAuth access tokens ya29.*
508
+ out = out.replace(/\bya29\.[A-Za-z0-9\-_]{20,}/g, "[REDACTED_GOOGLE_TOKEN]");
509
+
510
+ // 4. Google API keys AIza*
511
+ out = out.replace(/\bAIza[0-9A-Za-z\-_]{30,}/g, "[REDACTED_API_KEY]");
512
+
513
+ // 5. Generic secret=/password=/token= assignments with long values (>=12 chars)
514
+ out = out.replace(
515
+ /((?:secret|password|token)\s*[:=]\s*["']?)([^"'\s]{12,})/gi,
516
+ (_m, p1: string) => `${p1}[REDACTED]`,
517
+ );
518
+
519
+ return out;
520
+ }
521
+
522
+ /** Sensitive substrings for object-key based redaction (defense in depth). */
523
+ const SENSITIVE_KEY_SUBSTRINGS = [
524
+ "refresh_token",
525
+ "access_token",
526
+ "id_token",
527
+ "api_key",
528
+ "apikey",
529
+ "api-key",
530
+ "client_secret",
531
+ "client_secret_expires",
532
+ "password",
533
+ ] as const;
534
+
535
+ /** Legacy exact-key sensitive set used by {@link redactObject}. */
536
+ const LEGACY_SENSITIVE_KEYS = new Set([
537
+ "access_token",
538
+ "refresh_token",
539
+ "id_token",
540
+ "client_secret",
541
+ "clientSecret",
542
+ "accessToken",
543
+ "refreshToken",
544
+ "idToken",
545
+ "api_key",
546
+ "apikey",
547
+ "token",
548
+ "authorization",
549
+ "cookie",
550
+ "set-cookie",
551
+ "x-goog-api-key",
552
+ ]);
553
+
554
+ /** Checks whether an object key name is considered sensitive. */
555
+ function isSensitiveKey(key: string): boolean {
556
+ const low = key.toLowerCase();
557
+ return SENSITIVE_KEY_SUBSTRINGS.some((s) => low.includes(s));
558
+ }
559
+
560
+ /**
561
+ * Legacy shallow-cloning redaction: redacts sensitive keys and string values
562
+ * recursively (without circular-reference protection).
563
+ */
564
+ export function redactObject<T>(obj: T): T {
565
+ if (obj === null || obj === undefined) return obj;
566
+ if (typeof obj === "string") {
567
+ return redactSecretsPatterns(obj) as unknown as T;
568
+ }
569
+ if (typeof obj !== "object") return obj;
570
+ if (Array.isArray(obj)) {
571
+ return (obj as unknown as Array<unknown>).map(redactObject) as unknown as T;
572
+ }
573
+ const copy: Record<string, unknown> = {};
574
+ for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
575
+ if (LEGACY_SENSITIVE_KEYS.has(k.toLowerCase()) || LEGACY_SENSITIVE_KEYS.has(k)) {
576
+ copy[k] = "***REDACTED***";
577
+ } else if (typeof v === "string") {
578
+ copy[k] = redactSecretsPatterns(v);
579
+ } else if (typeof v === "object" && v !== null) {
580
+ copy[k] = redactObject(v);
581
+ } else {
582
+ copy[k] = v;
583
+ }
584
+ }
585
+ return copy as unknown as T;
586
+ }
587
+
588
+ /**
589
+ * Deep redaction that clones and redacts sensitive keys and string values.
590
+ * Handles circular references (replaced with "[Circular]"), Error objects
591
+ * (name/message/stack plus enumerable props) and arrays. Never throws.
592
+ *
593
+ * @param value - arbitrary value to clone/redact
594
+ * @param seen - internal WeakSet tracking visited objects
595
+ */
596
+ export function redactObjectDeep<T>(value: T, seen = new WeakSet<object>()): T {
597
+ if (value === null || value === undefined) return value;
598
+ const t = typeof value;
599
+ if (t === "string") {
600
+ return redactSecretsText(value as unknown as string) as unknown as T;
601
+ }
602
+ if (t !== "object") {
603
+ // number, boolean, bigint, symbol, function - returned as-is
604
+ return value;
605
+ }
606
+
607
+ // Circular reference guard
608
+ if (seen.has(value as unknown as object)) {
609
+ return "[Circular]" as unknown as T;
610
+ }
611
+
612
+ // Error special handling - capture name/message/stack redacted
613
+ if (value instanceof Error) {
614
+ seen.add(value as unknown as object);
615
+ const errObj: Record<string, unknown> = {
616
+ name: (value as Error).name,
617
+ message: redactSecretsText((value as Error).message),
618
+ };
619
+ const stack = (value as Error).stack;
620
+ if (stack) errObj.stack = redactSecretsText(stack);
621
+ for (const k of Object.keys(value as unknown as object)) {
622
+ if (k === "message" || k === "stack" || k === "name") continue;
623
+ try {
624
+ const v = (value as unknown as Record<string, unknown>)[k];
625
+ if (isSensitiveKey(k)) {
626
+ errObj[k] = "[REDACTED]";
627
+ } else {
628
+ errObj[k] = redactObjectDeep(v as unknown, seen);
629
+ }
630
+ } catch {
631
+ errObj[k] = "[Unserializable]";
632
+ }
633
+ }
634
+ return errObj as unknown as T;
635
+ }
636
+
637
+ // Arrays
638
+ if (Array.isArray(value)) {
639
+ seen.add(value as unknown as object);
640
+ const arr = value as unknown as unknown[];
641
+ const out: unknown[] = new Array(arr.length);
642
+ for (let i = 0; i < arr.length; i++) {
643
+ try {
644
+ out[i] = redactObjectDeep(arr[i] as unknown, seen);
645
+ } catch {
646
+ out[i] = "[Unserializable]";
647
+ }
648
+ }
649
+ return out as unknown as T;
650
+ }
651
+
652
+ // Plain objects / records
653
+ const obj = value as unknown as Record<string, unknown>;
654
+ seen.add(obj as unknown as object);
655
+ const cloned: Record<string, unknown> = {};
656
+ for (const key of Object.keys(obj)) {
657
+ try {
658
+ const v = obj[key];
659
+ if (isSensitiveKey(key)) {
660
+ cloned[key] = "[REDACTED]";
661
+ } else {
662
+ cloned[key] = redactObjectDeep(v as unknown, seen);
663
+ }
664
+ } catch {
665
+ cloned[key] = "[Unserializable]";
666
+ }
667
+ }
668
+ return cloned as unknown as T;
669
+ }
670
+
671
+ /**
672
+ * Safe JSON stringify that never throws; runs the deep redaction layer first.
673
+ */
674
+ function safeStringifyMeta(meta: unknown): string {
675
+ if (meta === undefined) return "";
676
+ if (meta === null) return "null";
677
+ try {
678
+ const redacted = redactObjectDeep(meta);
679
+ return JSON.stringify(redacted);
680
+ } catch {
681
+ try {
682
+ return String(meta);
683
+ } catch {
684
+ return "[Unserializable meta]";
685
+ }
686
+ }
687
+ }
688
+
689
+ // ---------------------------------------------------------------------------
690
+ // Section: TUI circular buffer
691
+ // ---------------------------------------------------------------------------
692
+
693
+ export interface LogEntry {
694
+ timestamp: string; // ISO string
695
+ level: LogLevel;
696
+ levelName: string;
697
+ message: string;
698
+ data?: unknown | undefined;
699
+ scope?: string | undefined;
700
+ }
701
+
702
+ export class CircularBuffer<T> {
703
+ private buf: (T | undefined)[];
704
+ private head = 0;
705
+ private count = 0;
706
+
707
+ constructor(public readonly capacity: number) {
708
+ this.buf = new Array<T | undefined>(capacity);
709
+ }
710
+
711
+ push(item: T): void {
712
+ this.buf[this.head] = item;
713
+ this.head = (this.head + 1) % this.capacity;
714
+ if (this.count < this.capacity) this.count++;
715
+ }
716
+
717
+ toArray(): T[] {
718
+ const result: T[] = [];
719
+ const start = this.count < this.capacity ? 0 : this.head;
720
+ for (let i = 0; i < this.count; i++) {
721
+ const idx = (start + i) % this.capacity;
722
+ const val = this.buf[idx];
723
+ if (val !== undefined) result.push(val);
724
+ }
725
+ return result;
726
+ }
727
+
728
+ clear(): void {
729
+ this.buf = new Array<T | undefined>(this.capacity);
730
+ this.head = 0;
731
+ this.count = 0;
732
+ }
733
+
734
+ get size(): number {
735
+ return this.count;
736
+ }
737
+
738
+ getRecent(n: number): T[] {
739
+ const all = this.toArray();
740
+ return all.slice(-n);
741
+ }
742
+ }
743
+
744
+ // ---------------------------------------------------------------------------
745
+ // Section: Logger singleton (merged feature set)
746
+ // ---------------------------------------------------------------------------
747
+
748
+ export interface DebugLoggerOptions {
749
+ logDir?: string;
750
+ level?: LogLevel | LogLevelName;
751
+ tuiEnabled?: boolean;
752
+ tuiCapacity?: number;
753
+ quietMode?: boolean;
754
+ enableFileLogging?: boolean;
755
+ /** Allows injecting env for tests */
756
+ env?: NodeJS.ProcessEnv;
757
+ }
758
+
759
+ export class DebugLogger {
760
+ private static _instance: DebugLogger | null = null;
761
+
762
+ public readonly logDir: string;
763
+ public readonly logFile: string;
764
+ private level: LogLevel;
765
+ /** Observer-style master switch: false means completely silenced. */
766
+ private enabled: boolean;
767
+ /** Raw debug env value last seen (for introspection). */
768
+ private rawEnv: string | undefined;
769
+ private tuiEnabled: boolean;
770
+ private quietMode: boolean;
771
+ private fileLoggingEnabled: boolean;
772
+ private buffer: CircularBuffer<LogEntry>;
773
+ private dirEnsured = false;
774
+ private lastCleanup = 0;
775
+ private readonly CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1h throttle
776
+ private writeCount = 0;
777
+
778
+ // Guards against recursion if fs fails while logging the failure itself.
779
+ private isWriting = false;
780
+
781
+ private constructor(opts: DebugLoggerOptions = {}) {
782
+ const env = opts.env ?? process.env;
783
+
784
+ this.logDir = opts.logDir ?? getDefaultLogDir();
785
+ this.logFile = getLogFilePath(this.logDir);
786
+
787
+ // Level control: OPENCODE_ANTIGRAVITY_DEBUG first, ANTIGRAVITY_DEBUG compat.
788
+ const envLevelRaw = readFirstEnv(env, ["OPENCODE_ANTIGRAVITY_DEBUG", "ANTIGRAVITY_DEBUG"]);
789
+ this.rawEnv = envLevelRaw;
790
+
791
+ if (opts.level !== undefined) {
792
+ this.level = typeof opts.level === "string" ? this.parseLevelOption(opts.level, LogLevel.INFO) : opts.level;
793
+ this.enabled = this.level !== LogLevel.SILENT;
794
+ } else if (envLevelRaw !== undefined) {
795
+ const parsed = parseAntigravityDebugEnv(envLevelRaw);
796
+ this.level = parsed.level;
797
+ this.enabled = parsed.enabled;
798
+ } else {
799
+ // Default INFO when neither option nor env is provided.
800
+ this.level = LogLevel.INFO;
801
+ this.enabled = true;
802
+ }
803
+
804
+ // TUI control: OPENCODE_ANTIGRAVITY_DEBUG_TUI first, ANTIGRAVITY_DEBUG_TUI compat.
805
+ const envTuiRaw = readFirstEnv(env, ["OPENCODE_ANTIGRAVITY_DEBUG_TUI", "ANTIGRAVITY_DEBUG_TUI"]);
806
+ if (opts.tuiEnabled !== undefined) {
807
+ this.tuiEnabled = opts.tuiEnabled;
808
+ } else if (envTuiRaw !== undefined) {
809
+ this.tuiEnabled = parseBooleanEnv(envTuiRaw, true);
810
+ } else {
811
+ // Default: TUI active whenever logging is not silent.
812
+ this.tuiEnabled = this.level !== LogLevel.SILENT;
813
+ }
814
+
815
+ // Quiet mode suppresses toast notifications.
816
+ const envQuietRaw =
817
+ env["OPENCODE_ANTIGRAVITY_QUIET_MODE"] ??
818
+ env["OPENCODE_ANTIGRAVITY_QUIET"] ??
819
+ env["OPENCODE_ANTIGRAVITY_SUPPRESS_TOAST"];
820
+ if (opts.quietMode !== undefined) {
821
+ this.quietMode = opts.quietMode;
822
+ } else {
823
+ this.quietMode = parseBooleanEnv(envQuietRaw, false);
824
+ }
825
+
826
+ this.fileLoggingEnabled = opts.enableFileLogging ?? true;
827
+
828
+ const capacity = opts.tuiCapacity ?? TUI_BUFFER_CAPACITY;
829
+ this.buffer = new CircularBuffer<LogEntry>(capacity);
830
+
831
+ // Best-effort boot: create dir, secure permissions, clean old logs.
832
+ try {
833
+ this.ensureLogDir();
834
+ this.cleanupOldLogs();
835
+ } catch {
836
+ // noop - never break the host app if fs fails at boot
837
+ }
838
+
839
+ // Bind for callback usage.
840
+ this.debug = this.debug.bind(this);
841
+ this.info = this.info.bind(this);
842
+ this.warn = this.warn.bind(this);
843
+ this.error = this.error.bind(this);
844
+ }
845
+
846
+ // -------------------------------------------------------------------------
847
+ // Singleton access
848
+ // -------------------------------------------------------------------------
849
+ public static getInstance(opts?: DebugLoggerOptions): DebugLogger {
850
+ if (!DebugLogger._instance) {
851
+ DebugLogger._instance = new DebugLogger(opts);
852
+ } else if (opts && Object.keys(opts).length > 0) {
853
+ // Existing instance plus explicit overrides - apply them.
854
+ DebugLogger._instance.applyOptions(opts);
855
+ }
856
+ return DebugLogger._instance;
857
+ }
858
+
859
+ /** For tests - resets the singleton. */
860
+ public static _resetForTests(): void {
861
+ DebugLogger._instance = null;
862
+ }
863
+
864
+ /**
865
+ * For testing only - resets the singleton and returns a fresh instance that
866
+ * reads current env. Production code should use getInstance().
867
+ */
868
+ public static resetInstanceForTests(): DebugLogger {
869
+ DebugLogger._instance = null;
870
+ return DebugLogger.getInstance();
871
+ }
872
+
873
+ private parseLevelOption(level: LogLevel | LogLevelName | string, fallback: LogLevel): LogLevel {
874
+ if (typeof level === "number") return level;
875
+ const low = String(level).trim().toLowerCase();
876
+ const mapped = LEVEL_NAME_TO_ENUM[low];
877
+ return mapped ?? fallback;
878
+ }
879
+
880
+ private applyOptions(opts: DebugLoggerOptions): void {
881
+ if (opts.level !== undefined) {
882
+ this.level = typeof opts.level === "string" ? this.parseLevelOption(opts.level, this.level) : opts.level;
883
+ this.enabled = this.level !== LogLevel.SILENT;
884
+ }
885
+ if (opts.tuiEnabled !== undefined) this.tuiEnabled = opts.tuiEnabled;
886
+ if (opts.quietMode !== undefined) this.quietMode = opts.quietMode;
887
+ if (opts.enableFileLogging !== undefined) this.fileLoggingEnabled = opts.enableFileLogging;
888
+ }
889
+
890
+ // -------------------------------------------------------------------------
891
+ // Getters / setters
892
+ // -------------------------------------------------------------------------
893
+ public getLevel(): LogLevel {
894
+ return this.level;
895
+ }
896
+
897
+ public getLevelName(): LogLevelName {
898
+ const name = LEVEL_ENUM_TO_NAME[this.level]?.toLowerCase();
899
+ return (name as LogLevelName) ?? "info";
900
+ }
901
+
902
+ /**
903
+ * Programmatic level setter - overrides env control for this process.
904
+ * Accepts enum values, level names, or any string understood by
905
+ * {@link parseAntigravityDebugEnv}.
906
+ */
907
+ public setLevel(level: LogLevel | LogLevelName | string): void {
908
+ try {
909
+ if (typeof level === "number") {
910
+ if (Object.values(LogLevel).includes(level as LogLevel)) {
911
+ this.level = level as LogLevel;
912
+ this.enabled = this.level !== LogLevel.SILENT;
913
+ }
914
+ return;
915
+ }
916
+ const low = String(level).trim().toLowerCase();
917
+ const mapped = LEVEL_NAME_TO_ENUM[low];
918
+ if (mapped !== undefined) {
919
+ this.level = mapped;
920
+ this.enabled = this.level !== LogLevel.SILENT;
921
+ return;
922
+ }
923
+ // Flexible parsing via the env parser (lists, wildcards, numbers).
924
+ const parsed = parseAntigravityDebugEnv(String(level));
925
+ this.level = parsed.level;
926
+ this.enabled = parsed.enabled;
927
+ } catch {
928
+ // ignore - setter must never throw
929
+ }
930
+ }
931
+
932
+ public isTuiEnabled(): boolean {
933
+ return this.tuiEnabled;
934
+ }
935
+
936
+ public setTuiEnabled(enabledFlag: boolean): void {
937
+ this.tuiEnabled = enabledFlag;
938
+ }
939
+
940
+ public isQuietMode(): boolean {
941
+ return this.quietMode;
942
+ }
943
+
944
+ public setQuietMode(quiet: boolean): void {
945
+ this.quietMode = quiet;
946
+ }
947
+
948
+ /**
949
+ * Whether toast notifications should be shown (quiet_mode suppresses them).
950
+ */
951
+ public shouldShowToast(): boolean {
952
+ return !this.quietMode;
953
+ }
954
+
955
+ public isDebugEnabled(): boolean {
956
+ return this.enabled && this.level <= LogLevel.DEBUG;
957
+ }
958
+
959
+ /** Whether file logging is currently enabled (false after disable tokens). */
960
+ public isEnabled(): boolean {
961
+ return this.enabled && this.fileLoggingEnabled;
962
+ }
963
+
964
+ /** Whether a given level would be emitted right now. */
965
+ public isLevelEnabled(lvl: LogLevel): boolean {
966
+ if (!this.enabled || this.level === LogLevel.SILENT) return false;
967
+ return lvl >= this.level;
968
+ }
969
+
970
+ /** Raw debug env value last observed. */
971
+ public getRawEnv(): string | undefined {
972
+ return this.rawEnv;
973
+ }
974
+
975
+ public getLogDir(): string {
976
+ return this.logDir;
977
+ }
978
+
979
+ public getLogFilePath(): string {
980
+ return this.logFile;
981
+ }
982
+
983
+ // -------------------------------------------------------------------------
984
+ // Filesystem - dir creation with hardening, rotation, retention cleanup
985
+ // -------------------------------------------------------------------------
986
+ private ensureLogDir(): void {
987
+ if (this.dirEnsured) return;
988
+ try {
989
+ if (!fs.existsSync(this.logDir)) {
990
+ fs.mkdirSync(this.logDir, { recursive: true, mode: 0o700 });
991
+ }
992
+ // Best-effort chmod 0o700 - silently ignored on Windows / restricted FS.
993
+ try {
994
+ fs.chmodSync(this.logDir, 0o700);
995
+ } catch {
996
+ // ignore chmod errors
997
+ }
998
+ this.dirEnsured = true;
999
+ } catch {
1000
+ // Never propagate - logging cannot break the app.
1001
+ this.dirEnsured = false;
1002
+ }
1003
+ }
1004
+
1005
+ /** Best-effort chmod 0o600 on the active log file. */
1006
+ private ensureLogFilePermissions(): void {
1007
+ try {
1008
+ if (fs.existsSync(this.logFile)) {
1009
+ fs.chmodSync(this.logFile, 0o600);
1010
+ }
1011
+ } catch {
1012
+ // ignore
1013
+ }
1014
+ }
1015
+
1016
+ private getFileSize(): number {
1017
+ try {
1018
+ const stat = fs.statSync(this.logFile);
1019
+ return stat.size;
1020
+ } catch {
1021
+ return 0;
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * Rotates the active file when it reaches DEBUG_LOG_MAX_BYTES. Renames to
1027
+ * antigravity-YYYY-MM-DD-HH-mm-ss-mmm.log, recreates an empty active file
1028
+ * with mode 0o600, and falls back to truncation if rename fails.
1029
+ */
1030
+ private rotateIfNeeded(): void {
1031
+ if (!this.fileLoggingEnabled) return;
1032
+ try {
1033
+ const size = this.getFileSize();
1034
+ if (size < MAX_FILE_SIZE_BYTES) return;
1035
+
1036
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").replace("Z", "");
1037
+ const rotatedName = `antigravity-${timestamp}.log`;
1038
+ const rotatedPath = path.join(this.logDir, rotatedName);
1039
+
1040
+ try {
1041
+ fs.renameSync(this.logFile, rotatedPath);
1042
+ // Recreate a fresh empty active file with secure mode.
1043
+ try {
1044
+ fs.writeFileSync(this.logFile, "", { encoding: "utf8", mode: 0o600 });
1045
+ this.ensureLogFilePermissions();
1046
+ } catch {
1047
+ // ignore - next append recreates it
1048
+ }
1049
+ } catch {
1050
+ // If rename fails (e.g. file open elsewhere), truncate instead.
1051
+ try {
1052
+ fs.truncateSync(this.logFile, 0);
1053
+ } catch {
1054
+ // ignore
1055
+ }
1056
+ }
1057
+
1058
+ // After rotation, try retention cleanup immediately.
1059
+ this.cleanupOldLogs(true);
1060
+ } catch {
1061
+ // ignore rotation errors
1062
+ }
1063
+ }
1064
+
1065
+ /**
1066
+ * Removes rotated logs older than DEBUG_LOG_RETENTION_DAYS days.
1067
+ * Called at boot, after rotation, and every CLEANUP_EVERY_WRITES writes;
1068
+ * throttled to run at most once per hour unless forced.
1069
+ */
1070
+ public cleanupOldLogs(force = false): void {
1071
+ const now = Date.now();
1072
+ if (!force && now - this.lastCleanup < this.CLEANUP_INTERVAL_MS) return;
1073
+ this.lastCleanup = now;
1074
+
1075
+ if (!this.fileLoggingEnabled) return;
1076
+ try {
1077
+ this.ensureLogDir();
1078
+ const files = fs.readdirSync(this.logDir);
1079
+ for (const file of files) {
1080
+ // Only handle our own naming pattern.
1081
+ if (!file.startsWith("antigravity") || !file.endsWith(".log")) continue;
1082
+ // Preserve the active file.
1083
+ if (file === LOG_FILE_NAME) continue;
1084
+
1085
+ const fullPath = path.join(this.logDir, file);
1086
+ try {
1087
+ const stat = fs.statSync(fullPath);
1088
+ const age = now - stat.mtimeMs;
1089
+ if (age > MAX_FILE_AGE_MS) {
1090
+ fs.unlinkSync(fullPath);
1091
+ }
1092
+ } catch {
1093
+ // Ignore individual file errors.
1094
+ }
1095
+ }
1096
+ } catch {
1097
+ // ignore
1098
+ }
1099
+ }
1100
+
1101
+ // -------------------------------------------------------------------------
1102
+ // Core logging pipeline
1103
+ // -------------------------------------------------------------------------
1104
+ private shouldLog(incomingLevel: LogLevel): boolean {
1105
+ if (!this.enabled) return false;
1106
+ if (this.level === LogLevel.SILENT) return false;
1107
+ return incomingLevel >= this.level;
1108
+ }
1109
+
1110
+ private formatEntry(entry: LogEntry): string {
1111
+ const redactedMessage = redactSecretsPatterns(entry.message);
1112
+ let line = `${entry.timestamp} [${entry.levelName}]`;
1113
+ if (entry.scope) {
1114
+ line += ` [${redactSecretsPatterns(entry.scope)}]`;
1115
+ }
1116
+ line += ` ${redactedMessage}`;
1117
+ if (entry.data !== undefined) {
1118
+ const json = safeStringifyMeta(entry.data);
1119
+ line += json ? ` ${json}` : ` [unserializable data]`;
1120
+ }
1121
+ return line + "\n";
1122
+ }
1123
+
1124
+ private writeToFile(formatted: string): void {
1125
+ if (!this.fileLoggingEnabled) return;
1126
+ if (this.isWriting) return; // recursion guard
1127
+ this.isWriting = true;
1128
+ try {
1129
+ this.ensureLogDir();
1130
+ this.rotateIfNeeded();
1131
+ fs.appendFileSync(this.logFile, formatted, { encoding: "utf8", mode: 0o600 });
1132
+ this.ensureLogFilePermissions();
1133
+
1134
+ // Retention check every CLEANUP_EVERY_WRITES writes (throttled internally).
1135
+ this.writeCount++;
1136
+ if (this.writeCount % CLEANUP_EVERY_WRITES === 0) {
1137
+ this.cleanupOldLogs();
1138
+ }
1139
+ } catch {
1140
+ // Logging failures never break the host app.
1141
+ } finally {
1142
+ this.isWriting = false;
1143
+ }
1144
+ }
1145
+
1146
+ private pushToTui(entry: LogEntry): void {
1147
+ if (!this.tuiEnabled) return;
1148
+ try {
1149
+ this.buffer.push(entry);
1150
+ } catch {
1151
+ // ignore
1152
+ }
1153
+ }
1154
+
1155
+ private log(level: LogLevel, message: string, data?: unknown, scope?: string): LogEntry | null {
1156
+ if (!this.shouldLog(level)) return null;
1157
+
1158
+ const entry: LogEntry = {
1159
+ timestamp: new Date().toISOString(),
1160
+ level,
1161
+ levelName: LEVEL_ENUM_TO_NAME[level] ?? "INFO",
1162
+ message: String(message),
1163
+ data,
1164
+ scope,
1165
+ };
1166
+
1167
+ // Store already-redacted copies in the TUI buffer for safety.
1168
+ const redactedForStorage: LogEntry = {
1169
+ ...entry,
1170
+ message: redactSecretsPatterns(entry.message),
1171
+ data: data !== undefined ? redactObject(data) : undefined,
1172
+ scope: scope ? redactSecretsPatterns(scope) : undefined,
1173
+ };
1174
+
1175
+ this.pushToTui(redactedForStorage);
1176
+
1177
+ const formatted = this.formatEntry(entry);
1178
+ this.writeToFile(formatted);
1179
+
1180
+ // Stderr echo: only at DEBUG level, warn/error lines, never in quiet mode,
1181
+ // so the opencode TUI stdout stays untouched.
1182
+ if (this.level <= LogLevel.DEBUG) {
1183
+ try {
1184
+ if (typeof process !== "undefined" && process.stderr && !this.quietMode) {
1185
+ if (level >= LogLevel.WARN) {
1186
+ process.stderr.write(formatted);
1187
+ }
1188
+ }
1189
+ } catch {
1190
+ // ignore
1191
+ }
1192
+ }
1193
+
1194
+ return redactedForStorage;
1195
+ }
1196
+
1197
+ // -------------------------------------------------------------------------
1198
+ // Public level methods
1199
+ // -------------------------------------------------------------------------
1200
+ public debug(message: string, data?: unknown, scope?: string): LogEntry | null {
1201
+ return this.log(LogLevel.DEBUG, message, data, scope);
1202
+ }
1203
+
1204
+ public info(message: string, data?: unknown, scope?: string): LogEntry | null {
1205
+ return this.log(LogLevel.INFO, message, data, scope);
1206
+ }
1207
+
1208
+ public warn(message: string, data?: unknown, scope?: string): LogEntry | null {
1209
+ return this.log(LogLevel.WARN, message, data, scope);
1210
+ }
1211
+
1212
+ public error(message: string, data?: unknown, scope?: string): LogEntry | null {
1213
+ return this.log(LogLevel.ERROR, message, data, scope);
1214
+ }
1215
+
1216
+ /**
1217
+ * Log with custom scope (useful for child loggers).
1218
+ */
1219
+ public withScope(scope: string): ScopedLogger {
1220
+ return new ScopedLogger(this, scope);
1221
+ }
1222
+
1223
+ // -------------------------------------------------------------------------
1224
+ // Env refresh - long-running processes may change env at runtime
1225
+ // -------------------------------------------------------------------------
1226
+ /**
1227
+ * Re-reads OPENCODE_ANTIGRAVITY_DEBUG (or ANTIGRAVITY_DEBUG) and updates
1228
+ * the enabled flag and level accordingly.
1229
+ *
1230
+ * @param env - optional env override (defaults to process.env)
1231
+ * @returns the fresh parse result
1232
+ */
1233
+ public refreshFromEnv(env: NodeJS.ProcessEnv = process.env): EnvParseResult {
1234
+ try {
1235
+ const raw = readFirstEnv(env, ["OPENCODE_ANTIGRAVITY_DEBUG", "ANTIGRAVITY_DEBUG"]);
1236
+ const parsed = parseAntigravityDebugEnv(raw);
1237
+ this.enabled = parsed.enabled;
1238
+ this.level = parsed.level;
1239
+ this.rawEnv = raw;
1240
+ return parsed;
1241
+ } catch {
1242
+ return { enabled: this.enabled, level: this.level, raw: this.rawEnv };
1243
+ }
1244
+ }
1245
+
1246
+ // -------------------------------------------------------------------------
1247
+ // TUI API
1248
+ // -------------------------------------------------------------------------
1249
+ public getTuiBuffer(): LogEntry[] {
1250
+ return this.buffer.toArray();
1251
+ }
1252
+
1253
+ public getRecentLogs(count: number): LogEntry[] {
1254
+ return this.buffer.getRecent(count);
1255
+ }
1256
+
1257
+ public getTuiBufferAsString(maxLines?: number): string {
1258
+ const entries = maxLines ? this.buffer.getRecent(maxLines) : this.buffer.toArray();
1259
+ return entries
1260
+ .map((e) => {
1261
+ const scopePart = e.scope ? ` [${e.scope}]` : "";
1262
+ const dataPart = e.data !== undefined ? ` ${JSON.stringify(e.data)}` : "";
1263
+ return `${e.timestamp} [${e.levelName}]${scopePart} ${e.message}${dataPart}`;
1264
+ })
1265
+ .join("\n");
1266
+ }
1267
+
1268
+ public clearTuiBuffer(): void {
1269
+ this.buffer.clear();
1270
+ }
1271
+
1272
+ public getTuiBufferSize(): number {
1273
+ return this.buffer.size;
1274
+ }
1275
+
1276
+ public getTuiCapacity(): number {
1277
+ return this.buffer.capacity;
1278
+ }
1279
+ }
1280
+
1281
+ // ---------------------------------------------------------------------------
1282
+ // Section: Scoped logger (per-module child loggers, e.g. auth:refresh)
1283
+ // ---------------------------------------------------------------------------
1284
+ export class ScopedLogger {
1285
+ constructor(
1286
+ private readonly parent: DebugLogger,
1287
+ private readonly scope: string,
1288
+ ) {}
1289
+
1290
+ debug(message: string, data?: unknown): LogEntry | null {
1291
+ return this.parent.debug(message, data, this.scope);
1292
+ }
1293
+ info(message: string, data?: unknown): LogEntry | null {
1294
+ return this.parent.info(message, data, this.scope);
1295
+ }
1296
+ warn(message: string, data?: unknown): LogEntry | null {
1297
+ return this.parent.warn(message, data, this.scope);
1298
+ }
1299
+ error(message: string, data?: unknown): LogEntry | null {
1300
+ return this.parent.error(message, data, this.scope);
1301
+ }
1302
+
1303
+ withScope(childScope: string): ScopedLogger {
1304
+ return new ScopedLogger(this.parent, `${this.scope}:${childScope}`);
1305
+ }
1306
+
1307
+ get parentLogger(): DebugLogger {
1308
+ return this.parent;
1309
+ }
1310
+ }
1311
+
1312
+ // ---------------------------------------------------------------------------
1313
+ // Section: Default singleton and functional helpers
1314
+ // ---------------------------------------------------------------------------
1315
+ function getLogger(): DebugLogger {
1316
+ return DebugLogger.getInstance();
1317
+ }
1318
+
1319
+ export const logger: DebugLogger = DebugLogger.getInstance();
1320
+
1321
+ /** Direct-import level helpers: import { debug, info } from "./debug" */
1322
+ export function debug(message: string, data?: unknown, scope?: string): LogEntry | null {
1323
+ return getLogger().debug(message, data, scope);
1324
+ }
1325
+ export function info(message: string, data?: unknown, scope?: string): LogEntry | null {
1326
+ return getLogger().info(message, data, scope);
1327
+ }
1328
+ export function warn(message: string, data?: unknown, scope?: string): LogEntry | null {
1329
+ return getLogger().warn(message, data, scope);
1330
+ }
1331
+ export function error(message: string, data?: unknown, scope?: string): LogEntry | null {
1332
+ return getLogger().error(message, data, scope);
1333
+ }
1334
+
1335
+ /** Creates a scoped logger (e.g. "auth:gemini", "plugin:load"). */
1336
+ export function createScopedLogger(scope: string): ScopedLogger {
1337
+ return getLogger().withScope(scope);
1338
+ }
1339
+
1340
+ /** Compatibility alias for createScopedLogger. */
1341
+ export function createDebugLogger(scope: string): ScopedLogger {
1342
+ return createScopedLogger(scope);
1343
+ }
1344
+
1345
+ /** State helpers controlled by env. */
1346
+ export function isDebugEnabled(): boolean {
1347
+ return getLogger().isDebugEnabled();
1348
+ }
1349
+ export function isTuiEnabled(): boolean {
1350
+ return getLogger().isTuiEnabled();
1351
+ }
1352
+ export function isQuietMode(): boolean {
1353
+ return getLogger().isQuietMode();
1354
+ }
1355
+ export function shouldShowToast(): boolean {
1356
+ return getLogger().shouldShowToast();
1357
+ }
1358
+
1359
+ /** TUI buffer access helpers. */
1360
+ export function getTuiBuffer(): LogEntry[] {
1361
+ return getLogger().getTuiBuffer();
1362
+ }
1363
+ export function getRecentLogs(n: number): LogEntry[] {
1364
+ return getLogger().getRecentLogs(n);
1365
+ }
1366
+ export function getTuiBufferAsString(maxLines?: number): string {
1367
+ return getLogger().getTuiBufferAsString(maxLines);
1368
+ }
1369
+ export function clearTuiBuffer(): void {
1370
+ return getLogger().clearTuiBuffer();
1371
+ }
1372
+
1373
+ /** Log filesystem info helpers. */
1374
+ export function getLogDir(): string {
1375
+ return getLogger().getLogDir();
1376
+ }
1377
+ export function getLogFilePathExport(): string {
1378
+ return getLogger().getLogFilePath();
1379
+ }
1380
+
1381
+ /** Dynamic adjustment helpers (useful for TUI commanders). */
1382
+ export function setLogLevel(level: LogLevel | LogLevelName): void {
1383
+ getLogger().setLevel(level);
1384
+ }
1385
+ export function setQuietMode(enabledFlag: boolean): void {
1386
+ getLogger().setQuietMode(enabledFlag);
1387
+ }
1388
+ export function setTuiEnabled(enabledFlag: boolean): void {
1389
+ getLogger().setTuiEnabled(enabledFlag);
1390
+ }
1391
+
1392
+ /**
1393
+ * Functional wrapper around {@link DebugLogger.refreshFromEnv}: re-reads the
1394
+ * debug env variables and applies them to the singleton logger.
1395
+ */
1396
+ export function refreshFromEnv(env?: NodeJS.ProcessEnv): EnvParseResult {
1397
+ return env ? getLogger().refreshFromEnv(env) : getLogger().refreshFromEnv();
1398
+ }
1399
+
1400
+ // ---------------------------------------------------------------------------
1401
+ // Default export - the DebugLogger class (construct your own instances).
1402
+ // The shared singleton is available as the named export `logger`.
1403
+ // ---------------------------------------------------------------------------
1404
+ export default DebugLogger;
1405
+
1406
+ // ===========================================================================
1407
+ // v2.1.15 Phase B: logging trio moved from config.ts (values verbatim —
1408
+ // the published ./system subpath semantics: "Bearer [REDACTED]" style)
1409
+ // ===========================================================================
1410
+
1411
+ /** Antigravity debug-logs directory inside the config directory. */
1412
+ export const logsDir = (): string => path.join(cfgDir(), "antigravity-logs");
1413
+
1414
+ /** Redacts Bearer JWTs and refresh_token substrings from a log line. */
1415
+ export const redactSecrets = (s: string): string =>
1416
+ s
1417
+ .replace(/Bearer\s+[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+/g, "Bearer [REDACTED]")
1418
+ .replace(/refresh_token[^\s]*/gi, "refresh_token=[REDACTED]");
1419
+
1420
+ /**
1421
+ * Mini debug-logger singleton: 1000-line ring buffer, daily log files under
1422
+ * logsDir(), 10MB rotation, 7-day cleanup, secret redaction on every line.
1423
+ */
1424
+ export class debugLogger {
1425
+ private static inst: debugLogger | null = null;
1426
+ private buffer: string[] = [];
1427
+ private maxBuf = 1000;
1428
+ static get(): debugLogger {
1429
+ if (!debugLogger.inst) debugLogger.inst = new debugLogger();
1430
+ return debugLogger.inst!;
1431
+ }
1432
+ log(msg: string): void {
1433
+ const line = `[${new Date().toISOString()}] ${redactSecrets(msg)}`;
1434
+ this.buffer.push(line);
1435
+ if (this.buffer.length > this.maxBuf) this.buffer.shift();
1436
+ try {
1437
+ fs.mkdirSync(logsDir(), { recursive: true });
1438
+ const p = path.join(logsDir(), `antigravity-${new Date().toISOString().slice(0, 10)}.log`);
1439
+ fs.writeFileSync(p, this.buffer.join("\n") + "\n", { flag: "a" });
1440
+ const st = fs.statSync(p);
1441
+ if (st.size > 10 * 1024 * 1024) {
1442
+ const bak = `${p}.1`;
1443
+ try {
1444
+ fs.renameSync(p, bak);
1445
+ } catch {
1446
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1447
+ }
1448
+ }
1449
+ } catch {
1450
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1451
+ }
1452
+ }
1453
+ getBuffer(): string[] {
1454
+ return [...this.buffer];
1455
+ }
1456
+ cleanup(): void {
1457
+ try {
1458
+ const files = fs.readdirSync(logsDir());
1459
+ const now = Date.now();
1460
+ for (const f of files) {
1461
+ const fp = path.join(logsDir(), f);
1462
+ try {
1463
+ const st = fs.statSync(fp);
1464
+ if (now - st.mtimeMs > 7 * 24 * 3600 * 1000) fs.unlinkSync(fp);
1465
+ } catch {
1466
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1467
+ }
1468
+ }
1469
+ } catch {
1470
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1471
+ }
1472
+ }
1473
+ }