@wenathlan/devthink 2.0.14 → 2.0.19

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 +60 -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/core.ts DELETED
@@ -1,1473 +0,0 @@
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
- }