@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/config.ts ADDED
@@ -0,0 +1,3137 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { homedir, platform } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ export type DevThinkConfig = {
6
+ activeProvider?: string | undefined;
7
+ activeModel?: string | undefined;
8
+ provider?: string;
9
+ model?: string;
10
+ mode?: string;
11
+ baseUrl?: string;
12
+ apiKey?: string;
13
+ anthropicApiKey?: string;
14
+ googleApiKey?: string;
15
+ zaiApiKey?: string;
16
+ temperature?: number;
17
+ maxTokens?: number;
18
+ projectMemory?: boolean;
19
+ externalMemory?: boolean;
20
+ fallbackProviders?: string[];
21
+ providers?: Record<
22
+ string,
23
+ {
24
+ baseUrl?: string;
25
+ apiKey?: string;
26
+ model?: string;
27
+ transport?: "official" | "openai-compatible" | "local-gateway";
28
+ auth?: { kind: "api-key" | "bearer" | "oauth"; value?: string; expiresAt?: number };
29
+ }
30
+ >;
31
+ gateway?: { mode?: "embedded"; stream?: boolean; host?: string };
32
+ web?: {
33
+ enabled?: boolean;
34
+ pagesUrl?: string;
35
+ gatewayUrl?: string;
36
+ allowedOrigins?: string[];
37
+ remoteSync?: { enabled?: boolean; endpoint?: string };
38
+ };
39
+ };
40
+
41
+ export type AuthKind = "api-key" | "bearer" | "oauth";
42
+ export type AuthCredential = {
43
+ kind: AuthKind;
44
+ value?: string;
45
+ accessToken?: string;
46
+ refreshToken?: string;
47
+ expiresAt?: number | undefined;
48
+ resourceUrl?: string;
49
+ updatedAt: string;
50
+ };
51
+ export type DevThinkAuth = { version: 1; providers: Record<string, AuthCredential> };
52
+
53
+ export type DevThinkPaths = {
54
+ home: string;
55
+ config: string;
56
+ auth: string;
57
+ identity: string;
58
+ pairings: string;
59
+ legacyConfig: string;
60
+ sessions: string;
61
+ workspaces: string;
62
+ database: string;
63
+ legacyDatabase: string;
64
+ memory: string;
65
+ logs: string;
66
+ };
67
+
68
+ function resolveHome(): string {
69
+ const override = process.env.DEVTHINK_HOME?.trim();
70
+ if (override) return override;
71
+ if (platform() === "win32") return join(process.env.APPDATA || homedir(), "devthink");
72
+ if (platform() === "darwin") return join(homedir(), "Library", "Application Support", "devthink");
73
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "devthink");
74
+ }
75
+
76
+ export function resolvePaths(root = resolveHome()): DevThinkPaths {
77
+ return {
78
+ home: root,
79
+ config: join(root, "devthink.json"),
80
+ auth: join(root, "auth.json"),
81
+ identity: join(root, "identity.json"),
82
+ pairings: join(root, "pairings.json"),
83
+ legacyConfig: join(root, "config.json"),
84
+ sessions: join(root, "sessions"),
85
+ workspaces: join(root, "workspaces"),
86
+ database: join(root, "devthink.db"),
87
+ legacyDatabase: join(root, "devthink.sqlite"),
88
+ memory: join(root, "memory"),
89
+ logs: join(root, "logs"),
90
+ };
91
+ }
92
+
93
+ export function ensurePaths(paths = resolvePaths()): DevThinkPaths {
94
+ for (const path of [paths.home, paths.sessions, paths.workspaces, paths.memory, paths.logs])
95
+ mkdirSync(path, { recursive: true });
96
+ if (!existsSync(paths.database) && existsSync(paths.legacyDatabase)) {
97
+ try {
98
+ copyFileSync(paths.legacyDatabase, paths.database);
99
+ } catch {
100
+ /* The JSON session records remain the safe compatibility fallback. */
101
+ }
102
+ }
103
+ return paths;
104
+ }
105
+
106
+ function isRecord(value: unknown): value is Record<string, unknown> {
107
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
108
+ }
109
+
110
+ function writeJson(path: string, value: unknown, mode?: number): void {
111
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
112
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, mode ? { encoding: "utf8", mode } : "utf8");
113
+ renameSync(temporary, path);
114
+ }
115
+
116
+ export function readConfig(paths = resolvePaths()): DevThinkConfig {
117
+ try {
118
+ const candidate = existsSync(paths.config) ? paths.config : paths.legacyConfig;
119
+ if (!existsSync(candidate)) return {};
120
+ const parsed: unknown = JSON.parse(readFileSync(candidate, "utf8"));
121
+ if (!isRecord(parsed)) return {};
122
+ const config = parsed as DevThinkConfig;
123
+ return {
124
+ ...config,
125
+ activeProvider: config.activeProvider || config.provider,
126
+ activeModel: config.activeModel || config.model,
127
+ };
128
+ } catch {
129
+ return {};
130
+ }
131
+ }
132
+
133
+ export function saveConfig(config: DevThinkConfig, paths = resolvePaths()): void {
134
+ ensurePaths(paths);
135
+ writeJson(paths.config, {
136
+ ...config,
137
+ activeProvider: config.activeProvider || config.provider,
138
+ activeModel: config.activeModel || config.model,
139
+ gateway: { mode: "embedded" as const, stream: true, ...config.gateway },
140
+ });
141
+ }
142
+
143
+ const forbiddenAuthFields = new Set([
144
+ "cookie",
145
+ "cookies",
146
+ "fingerprint",
147
+ "useragent",
148
+ "sessiontoken",
149
+ "captcha",
150
+ "browserprofile",
151
+ ]);
152
+
153
+ function validCredential(value: unknown): AuthCredential | undefined {
154
+ if (!isRecord(value) || Object.keys(value).some((key) => forbiddenAuthFields.has(key.toLowerCase())))
155
+ return undefined;
156
+ const kind = String(value.kind) as AuthKind;
157
+ if (!["api-key", "bearer", "oauth"].includes(kind)) return undefined;
158
+ const token = typeof value.value === "string" ? value.value : undefined;
159
+ const accessToken = typeof value.accessToken === "string" ? value.accessToken : undefined;
160
+ const refreshToken = typeof value.refreshToken === "string" ? value.refreshToken : undefined;
161
+ if ((kind === "oauth" && !accessToken) || (kind !== "oauth" && !token)) return undefined;
162
+ return {
163
+ kind,
164
+ ...(token ? { value: token } : {}),
165
+ ...(accessToken ? { accessToken } : {}),
166
+ ...(refreshToken ? { refreshToken } : {}),
167
+ ...(typeof value.expiresAt === "number" ? { expiresAt: value.expiresAt } : {}),
168
+ ...(typeof value.resourceUrl === "string" ? { resourceUrl: value.resourceUrl } : {}),
169
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : new Date().toISOString(),
170
+ };
171
+ }
172
+
173
+ export function readAuth(paths = resolvePaths()): DevThinkAuth {
174
+ try {
175
+ if (!existsSync(paths.auth)) return { version: 1, providers: {} };
176
+ const parsed: unknown = JSON.parse(readFileSync(paths.auth, "utf8"));
177
+ if (!isRecord(parsed) || !isRecord(parsed.providers)) return { version: 1, providers: {} };
178
+ const providers: Record<string, AuthCredential> = {};
179
+ for (const [provider, raw] of Object.entries(parsed.providers)) {
180
+ const credential = validCredential(raw);
181
+ if (credential) providers[provider.toLowerCase()] = credential;
182
+ }
183
+ return { version: 1, providers };
184
+ } catch {
185
+ return { version: 1, providers: {} };
186
+ }
187
+ }
188
+
189
+ export function saveAuth(auth: DevThinkAuth, paths = resolvePaths()): void {
190
+ ensurePaths(paths);
191
+ const providers: Record<string, AuthCredential> = {};
192
+ for (const [provider, raw] of Object.entries(auth.providers)) {
193
+ const credential = validCredential(raw);
194
+ if (credential) providers[provider.toLowerCase()] = credential;
195
+ }
196
+ writeJson(paths.auth, { version: 1, providers }, 0o600);
197
+ }
198
+
199
+ export function setAuthCredential(
200
+ provider: string,
201
+ credential: Omit<AuthCredential, "updatedAt">,
202
+ paths = resolvePaths(),
203
+ ): DevThinkAuth {
204
+ const next = { ...credential, updatedAt: new Date().toISOString() };
205
+ if (!validCredential(next))
206
+ throw new Error("Credential must use an official api-key, bearer, or OAuth shape without browser-session fields.");
207
+ const current = readAuth(paths);
208
+ const auth = { version: 1 as const, providers: { ...current.providers, [provider.toLowerCase()]: next } };
209
+ saveAuth(auth, paths);
210
+ return auth;
211
+ }
212
+
213
+ export function clearAuthCredential(provider: string, paths = resolvePaths()): DevThinkAuth {
214
+ const current = readAuth(paths);
215
+ const providers = { ...current.providers };
216
+ delete providers[provider.toLowerCase()];
217
+ const auth = { version: 1 as const, providers };
218
+ saveAuth(auth, paths);
219
+ return auth;
220
+ }
221
+
222
+ export function migrateLegacyCredentials(
223
+ config: DevThinkConfig,
224
+ paths = resolvePaths(),
225
+ ): { config: DevThinkConfig; migrated: string[] } {
226
+ const current = readAuth(paths);
227
+ const providers = { ...current.providers };
228
+ const nextConfig: DevThinkConfig = { ...config, providers: { ...config.providers } };
229
+ const migrated: string[] = [];
230
+ for (const [provider, settings] of Object.entries(config.providers || {})) {
231
+ const legacy = settings.auth?.value || settings.apiKey;
232
+ if (!legacy || providers[provider]) continue;
233
+ const kind = settings.auth?.kind || "api-key";
234
+ providers[provider] =
235
+ kind === "oauth"
236
+ ? { kind, accessToken: legacy, expiresAt: settings.auth?.expiresAt, updatedAt: new Date().toISOString() }
237
+ : { kind, value: legacy, expiresAt: settings.auth?.expiresAt, updatedAt: new Date().toISOString() };
238
+ const { auth: _auth, apiKey: _apiKey, ...rest } = settings;
239
+ nextConfig.providers![provider] = rest;
240
+ migrated.push(provider);
241
+ }
242
+ for (const [provider, property] of Object.entries({
243
+ openai: "apiKey",
244
+ anthropic: "anthropicApiKey",
245
+ google: "googleApiKey",
246
+ zai: "zaiApiKey",
247
+ })) {
248
+ const value = nextConfig[property as keyof DevThinkConfig];
249
+ if (typeof value === "string" && value && !providers[provider]) {
250
+ providers[provider] = { kind: "api-key", value, updatedAt: new Date().toISOString() };
251
+ delete (nextConfig as Record<string, unknown>)[property];
252
+ migrated.push(provider);
253
+ }
254
+ }
255
+ saveAuth({ version: 1, providers }, paths);
256
+ saveConfig(nextConfig, paths);
257
+ return { config: nextConfig, migrated };
258
+ }
259
+
260
+ export function getConfigValue(config: DevThinkConfig, key: string): unknown {
261
+ return (config as Record<string, unknown>)[key];
262
+ }
263
+
264
+ export function setConfigValue(config: DevThinkConfig, key: string, value: unknown): DevThinkConfig {
265
+ return { ...config, [key]: value };
266
+ }
267
+
268
+ export function resolveCredential(
269
+ provider: string,
270
+ config: DevThinkConfig,
271
+ paths = resolvePaths(),
272
+ ): string | undefined {
273
+ const auth = readAuth(paths).providers[provider.toLowerCase()];
274
+ if (auth) {
275
+ if (auth.kind === "oauth" && auth.expiresAt && auth.expiresAt <= Date.now())
276
+ throw new Error(
277
+ `Official OAuth credential for ${provider} has expired. Reauthenticate with the provider-owned flow.`,
278
+ );
279
+ return auth.kind === "oauth" ? auth.accessToken : auth.value;
280
+ }
281
+ const envNames: Record<string, string[]> = {
282
+ openai: ["OPENAI_API_KEY"],
283
+ anthropic: ["ANTHROPIC_API_KEY"],
284
+ google: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
285
+ zai: ["ZAI_API_KEY"],
286
+ qwen: ["QWEN_API_KEY"],
287
+ openrouter: ["OPENROUTER_API_KEY"],
288
+ deepseek: ["DEEPSEEK_API_KEY"],
289
+ groq: ["GROQ_API_KEY"],
290
+ mistral: ["MISTRAL_API_KEY"],
291
+ xai: ["XAI_API_KEY"],
292
+ ollama: ["OLLAMA_API_KEY"],
293
+ mimo: ["MIMO_API_KEY"],
294
+ };
295
+ for (const name of envNames[provider.toLowerCase()] || []) {
296
+ const value = process.env[name]?.trim();
297
+ if (value) return value;
298
+ }
299
+ const property = `${provider}ApiKey` as keyof DevThinkConfig;
300
+ const nested = config.providers?.[provider];
301
+ const legacy = config[property];
302
+ const legacyValue = typeof legacy === "string" ? legacy : undefined;
303
+ return (
304
+ nested?.auth?.value ||
305
+ nested?.apiKey ||
306
+ legacyValue ||
307
+ (provider === (config.activeProvider || config.provider) ? config.apiKey : undefined) ||
308
+ process.env.DEVTHINK_API_KEY?.trim()
309
+ );
310
+ }
311
+
312
+ export function redactValue(value: unknown): unknown {
313
+ if (typeof value !== "string") return value;
314
+ if (value.length < 8) return "[redacted]";
315
+ return `${value.slice(0, 3)}...${value.slice(-3)}`;
316
+ }
317
+
318
+ export function redactAuth(auth: DevThinkAuth): Record<string, unknown> {
319
+ return {
320
+ version: auth.version,
321
+ providers: Object.fromEntries(
322
+ Object.entries(auth.providers).map(([provider, credential]) => [
323
+ provider,
324
+ {
325
+ kind: credential.kind,
326
+ ...(credential.value ? { value: redactValue(credential.value) } : {}),
327
+ ...(credential.accessToken ? { accessToken: redactValue(credential.accessToken) } : {}),
328
+ ...(credential.refreshToken ? { refreshToken: redactValue(credential.refreshToken) } : {}),
329
+ ...(credential.expiresAt ? { expiresAt: credential.expiresAt } : {}),
330
+ ...(credential.resourceUrl ? { resourceUrl: credential.resourceUrl } : {}),
331
+ updatedAt: credential.updatedAt,
332
+ },
333
+ ]),
334
+ ),
335
+ };
336
+ }
337
+
338
+ export function redactConfig(config: DevThinkConfig): Record<string, unknown> {
339
+ const sensitive = new Set(["apiKey", "anthropicApiKey", "googleApiKey", "zaiApiKey"]);
340
+ return Object.fromEntries(
341
+ Object.entries(config).map(([key, value]) => {
342
+ if (key === "providers" && value && typeof value === "object") {
343
+ const providers = Object.fromEntries(
344
+ Object.entries(value as Record<string, Record<string, unknown>>).map(([id, settings]) => [
345
+ id,
346
+ Object.fromEntries(
347
+ Object.entries(settings).map(([setting, item]) => {
348
+ if (setting === "apiKey") return [setting, redactValue(item)];
349
+ if (setting === "auth" && item && typeof item === "object") {
350
+ const auth = item as Record<string, unknown>;
351
+ return [setting, { ...auth, value: redactValue(auth.value) }];
352
+ }
353
+ return [setting, item];
354
+ }),
355
+ ),
356
+ ]),
357
+ );
358
+ return [key, providers];
359
+ }
360
+ return [key, sensitive.has(key) || key.toLowerCase().endsWith("apikey") ? redactValue(value) : value];
361
+ }),
362
+ );
363
+ }
364
+
365
+ export function parseConfigValue(raw: string): unknown {
366
+ if (raw === "true") return true;
367
+ if (raw === "false") return false;
368
+ if (/^-?\d+$/.test(raw)) return Number.parseInt(raw, 10);
369
+ if (/^-?\d+\.\d+$/.test(raw)) return Number.parseFloat(raw);
370
+ if (raw.startsWith("[") || raw.startsWith("{")) {
371
+ try {
372
+ return JSON.parse(raw);
373
+ } catch {
374
+ return raw;
375
+ }
376
+ }
377
+ return raw;
378
+ }
379
+
380
+ /* ════════════════════════════════════════════════════════════════════
381
+ Section: the provider antigravity configuration (the 2.1.16 config owner
382
+ absorbed by the grand merge — validators, atomic writes, the embedded
383
+ JSON schema and the antigravity runtime defaults ride as ordinal
384
+ sections of the merged configuration domain; zero symbol overlap).
385
+ ════════════════════════════════════════════════════════════════════ */
386
+ /**
387
+ * @file config.ts
388
+ * @module provider/config
389
+ * @description
390
+ * THE single configuration-management module of the merged provider lineage — v2.1.14
391
+ * consolidation of config.ts + validate.ts + system.ts into one file
392
+ * (one file per correlated context, per the governance skill).
393
+ * Merged configuration module — v2 schema-driven base with the complete
394
+ * v1 surface ported on top. Nothing from either version was dropped.
395
+ *
396
+ * Third-person observer: the library watches how Antigravity Manager
397
+ * and Gemini CLI persist user preferences and reproduces that contract
398
+ * without depending on their runtime. The observer never leaks secrets,
399
+ * validates production inputs strictly, and enforces chmod 600 on disk.
400
+ *
401
+ * Layout: library-first, root-first.
402
+ * Runtime: only node:* builtins + global fetch (Node >=18). Zero external deps.
403
+ *
404
+ * Merge notes (conflict resolutions):
405
+ * - v2.1.14 consolidation : the validation helpers (former
406
+ * validate.ts, deleted) and the system utilities (former system.ts, now a
407
+ * pure compatibility re-export shim for the published ./system subpath)
408
+ * live in the trailing sections of this file. Canonical names resolve to
409
+ * the hardened implementations; legacy names survive as documented
410
+ * aliases or renames.
411
+ * - loadConfig / agsaveConfig : v1 synchronous cached API keeps the
412
+ * original names because existing modules import them synchronously.
413
+ * The minimal raw JSON IO helpers of the former system.ts survive under
414
+ * the renamed loadConfigRaw / saveConfigRaw.
415
+ * - loadConfigAsync / saveConfigAsync: v2 asynchronous implementations are
416
+ * exposed under these names (the names already existed in v1 as thin
417
+ * wrappers; the v2 implementations absorb that contract).
418
+ * - AntigravityConfig : single interface covering both the
419
+ * v1 field set and the v2 field set.
420
+ * - ConfigValidationError : single class accepting both the v1
421
+ * constructor (string error list) and the v2 constructor
422
+ * (message + structured ValidationError list).
423
+ * - isValidEmail / isValidSemver /
424
+ * isValidProjectId / validateAccount / validateModelId: canonical hardened
425
+ * validators from the former validate.ts (ReDoS-safe, documented).
426
+ * - validateEmail / validateSemver /
427
+ * validateProjectId : legacy system.ts names kept as
428
+ * one-line aliases of the canonical validators above.
429
+ * - validateModelIdCharset : former system.ts validateModelId
430
+ * charset regex variant, renamed because the canonical validateModelId
431
+ * is the semantic gemini/claude/antigravity check.
432
+ *
433
+ * Contract:
434
+ * - Path: ~/.config/opencode/antigravity.json (honors OPENCODE_CONFIG_DIR
435
+ * and XDG_CONFIG_HOME)
436
+ * - Schema: JSON Schema Draft-07 compatible
437
+ * - Persistence: atomic write (tmp + chmod 600 + rename), dir 0755/0700
438
+ * - Defaults: cli_first, rotation round-robin, quota_refresh 15min,
439
+ * google_search enabled, debug off
440
+ *
441
+ * Bypass metadata documented in schema description:
442
+ * Antigravity bypass relies on mimicking antigravity/{ver} {os}/{arch} UA
443
+ * and bypassing x-goog-user-project stripping, endpoint cascade 403/404/5xx.
444
+ *
445
+ * @author devthink
446
+ * @license MIT
447
+ * @version 2.1.16
448
+ */
449
+
450
+ import * as fs from "node:fs";
451
+ import * as fsp from "node:fs/promises";
452
+ import * as path from "node:path";
453
+ import * as os from "node:os";
454
+ import * as crypto from "node:crypto";
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // Gemini CLI bypass identity constants (v1) — OAuth client, headers, models
458
+ // ---------------------------------------------------------------------------
459
+
460
+ /**
461
+ * OAuth2 Client ID of the Gemini CLI (Google Cloud Code).
462
+ * @readonly
463
+ */
464
+ // v2.1.15 Phase C: OAUTH_CLIENT_ID (gemini-cli pair) moved to constants.js
465
+ // as GEMINI_CLI_OAUTH_CLIENT_ID — imported and re-exported below.
466
+
467
+ /**
468
+ * OAuth2 Client Secret of the Gemini CLI.
469
+ * @readonly
470
+ * @security This secret is public inside the Gemini CLI binary; it is not a private secret.
471
+ */
472
+
473
+ /**
474
+ * OAuth scopes required for Cloud Code + identity.
475
+ * @readonly
476
+ */
477
+ // v2.1.15 Phase C: OAUTH_SCOPES (3-scope Gemini CLI set) is constants.js
478
+ // GEMINI_CLI_SCOPES — imported from the owner.
479
+
480
+ /**
481
+ * OAuth scopes as a space-separated string, ready for token requests.
482
+ */
483
+ export const OAUTH_SCOPE_STRING = OAUTH_SCOPES.join(" ");
484
+
485
+ /**
486
+ * Canonical Gemini CLI User-Agent.
487
+ * Format: GeminiCLI/<version>/<default-model> (<platform>; <arch>; GitHub) google-api-nodejs-client/<gaxios-ver>
488
+ * Note: platform varies to avoid static fingerprinting.
489
+ * @readonly
490
+ */
491
+ // v2.1.15 Phase C: GEMINI_CLI_USER_AGENT (full UA value) was
492
+ // byte-identical to constants.GEMINI_CLI_USER_AGENT — imported from the owner.
493
+
494
+ /** Fingerprinted google-api-nodejs-client version. */
495
+ export const GOOGLE_API_CLIENT_VERSION = "9.15.1" as const;
496
+
497
+ /** Fingerprinted GeminiCLI version. */
498
+ export const GEMINI_CLI_VERSION = "0.57.0" as const;
499
+
500
+ /** Default model announced in the User-Agent. */
501
+ export const GEMINI_CLI_DEFAULT_MODEL = "gemini-3-pro-preview" as const;
502
+
503
+ /**
504
+ * X-Goog-Api-Client header value (Node.js runtime fingerprint).
505
+ * @readonly
506
+ */
507
+
508
+ /**
509
+ * Client metadata used for bypass (IDE_UNSPECIFIED = pure CLI).
510
+ * @readonly
511
+ */
512
+
513
+ /**
514
+ * Client-Metadata serialized as an `X-Goog-Client-Metadata` header string / payload.
515
+ */
516
+
517
+ /**
518
+ * Project fallback when the cloud project cannot be resolved via loadCodeAssist.
519
+ * @readonly
520
+ */
521
+
522
+ /** Cloud Code Assist PA (Playground API) base endpoints. */
523
+
524
+ // v2.1.15 Phase A: MODELS_2026 (the 11-id legacy bypass list) and the
525
+ // Model2026 union moved to models.ts (MODELS_2026_LEGACY / Model2026) —
526
+ // imported under the historical name and re-exported so the public surface
527
+ // is unchanged.
528
+ import { MODELS_2026_LEGACY as MODELS_2026 } from "./models.js";
529
+ import type { RotationStrategy } from "./accounts.js";
530
+ export type { RotationStrategy };
531
+ // v2.1.15 Phase B: identity masquerade (gemini UA builder, gl-node api-client
532
+ // snapshot) comes from fingerprint.js; raw shared values (client metadata,
533
+ // project fallback, cloudcode endpoints) come from constants.js.
534
+ import { buildGeminiUserAgent, X_GOOG_API_CLIENT_GEMINI_CLI as X_GOOG_API_CLIENT } from "./fingerprint.js";
535
+ import {
536
+ CLIENT_METADATA,
537
+ CLIENT_METADATA_STRING,
538
+ CLOUDCODE_BASE_URL,
539
+ CLOUDCODE_ENDPOINTS,
540
+ GEMINI_CLI_OAUTH_CLIENT_ID as OAUTH_CLIENT_ID,
541
+ GEMINI_CLI_OAUTH_CLIENT_SECRET as OAUTH_CLIENT_SECRET,
542
+ GEMINI_CLI_SCOPES as OAUTH_SCOPES,
543
+ GEMINI_CLI_USER_AGENT,
544
+ PROJECT_FALLBACK,
545
+ } from "./constants.js";
546
+ import type { Model2026 } from "./models.js";
547
+ export { MODELS_2026 };
548
+ export type { Model2026 };
549
+
550
+ // ---------------------------------------------------------------------------
551
+ // Dynamic user-agent helpers (v1) — platform variation anti-fingerprinting
552
+ // ---------------------------------------------------------------------------
553
+
554
+ /**
555
+ * Normalized platform tuple used for UA fingerprinting.
556
+ * Converts Node os.platform()/arch() into the strings Google expects.
557
+ */
558
+ export type PlatformTuple = {
559
+ platform: string;
560
+ arch: string;
561
+ };
562
+
563
+ /**
564
+ * Returns the platform/arch tuple in the format used by the official UA.
565
+ * Example: linux -> linux, darwin -> darwin, win32 -> win32
566
+ * @returns {PlatformTuple}
567
+ */
568
+ export function getPlatformTuple(): PlatformTuple {
569
+ const p = os.platform(); // linux, darwin, win32
570
+ const a = os.arch(); // x64, arm64, etc.
571
+
572
+ // Normalize arch to what the Gemini CLI announces
573
+ let archNormalized = a;
574
+ if (a === "x64") archNormalized = "x64";
575
+ else if (a === "arm64") archNormalized = "arm64";
576
+ else if (a === "arm") archNormalized = "arm";
577
+ // keep other values unchanged
578
+
579
+ // Platform is already normalized; just enforce lower-case
580
+ const platformNormalized = p.toLowerCase();
581
+
582
+ return { platform: platformNormalized, arch: archNormalized };
583
+ }
584
+
585
+ /**
586
+ * Builds the GeminiCLI User-Agent dynamically, varying the platform.
587
+ * Avoids static fingerprinting at the WAF level.
588
+ *
589
+ * Template: GeminiCLI/<cliVersion>/<model> (<platform>; <arch>; GitHub) google-api-nodejs-client/<gclientVersion>
590
+ *
591
+ * @param overrides - optional override of platform / cliVersion / model
592
+ * @returns User-Agent string ready for the `User-Agent` header
593
+ */
594
+
595
+ /**
596
+ * Builds a dynamic X-Goog-Api-Client header based on the current Node version,
597
+ * falling back to the static constant when process.versions is unavailable.
598
+ */
599
+ export function buildXGoogApiClient(): string {
600
+ try {
601
+ const nodeVer = typeof process !== "undefined" && process.versions?.node ? process.versions.node : "22.19.0";
602
+ return `gl-node/${nodeVer}`;
603
+ } catch {
604
+ return X_GOOG_API_CLIENT;
605
+ }
606
+ }
607
+
608
+ /** Complete identification headers for bypass. */
609
+ export function getBypassHeaders(): Record<string, string> {
610
+ return {
611
+ "User-Agent": buildGeminiUserAgent(),
612
+ "X-Goog-Api-Client": buildXGoogApiClient(),
613
+ "Client-Metadata": CLIENT_METADATA_STRING,
614
+ "X-Goog-Client-Metadata": CLIENT_METADATA_STRING,
615
+ };
616
+ }
617
+
618
+ // ---------------------------------------------------------------------------
619
+ // File system constants — observer enforces secure perms
620
+ // ---------------------------------------------------------------------------
621
+
622
+ const CONFIG_FILE_NAME = "antigravity.json" as const;
623
+ /* The literal rides the constant instead of the top-level path.join the devthink lineage computed at import time: the grand merge folded this module into the shared core graph the platform-portable bundles ship, and a stubbed browser bundle must never call a node builtin at module evaluation. */
624
+ const CONFIG_DIR_TAIL = ".config/opencode";
625
+ const FILE_MODE = 0o600 as const;
626
+ const DIR_MODE = 0o755 as const;
627
+ const CONFIG_DIR_NAME = "opencode" as const;
628
+
629
+ // ---------------------------------------------------------------------------
630
+ // Rotation strategy — requirement: round-robin / sticky
631
+ // ---------------------------------------------------------------------------
632
+
633
+ // v2.1.15 Phase C: RotationStrategy moved to accounts.js (rotation owner) —
634
+ // imported type-only (erased at runtime; no cycle with accounts.js).
635
+
636
+ export const ROTATION_STRATEGIES = ["round-robin", "sticky"] as const;
637
+
638
+ // ---------------------------------------------------------------------------
639
+ // Toast scope and quota fallback modes (v1)
640
+ // ---------------------------------------------------------------------------
641
+
642
+ /** Notification/toast display scope for opencode. */
643
+ export type ToastScope = "all" | "project" | "minimal" | "none";
644
+
645
+ /**
646
+ * Quota fallback mode.
647
+ * - true: enables automatic fallback to a reserve model
648
+ * - false: disables fallback, hard failure on 429
649
+ * - "auto": system picks the best model with remaining quota
650
+ * - string: specific model name to fall back to
651
+ */
652
+ export type QuotaFallback = boolean | "auto" | string;
653
+
654
+ // ---------------------------------------------------------------------------
655
+ // Debug — boolean or object shape, production tolerant
656
+ // ---------------------------------------------------------------------------
657
+
658
+ export type DebugLevel = "debug" | "info" | "warn" | "error" | "silent";
659
+
660
+ export interface DebugObjectConfig {
661
+ /** enable debug logging */
662
+ enabled?: boolean;
663
+ /** log level */
664
+ level?: DebugLevel;
665
+ /** custom log file path */
666
+ log_file?: string | null;
667
+ /** retention days */
668
+ retain_days?: number;
669
+ /** max bytes before rotation, e.g. 10MB */
670
+ max_bytes?: number;
671
+ /** TUI circular buffer lines */
672
+ tui_buffer_lines?: number;
673
+ /** redact secrets in logs */
674
+ redact_secrets?: boolean;
675
+ }
676
+
677
+ export type DebugConfig = boolean | DebugObjectConfig;
678
+
679
+ // ---------------------------------------------------------------------------
680
+ // Google Search grounding — requirement google_search enabled
681
+ // ---------------------------------------------------------------------------
682
+
683
+ export interface GoogleSearchConfig {
684
+ /** enable google_search tool injection */
685
+ enabled?: boolean;
686
+ /** model used for search grounding, defaults to gemini-3-flash */
687
+ model?: string;
688
+ /** enable url_context companion tool */
689
+ url_context?: boolean;
690
+ /** include groundingMetadata in response */
691
+ include_grounding_metadata?: boolean;
692
+ }
693
+
694
+ // ---------------------------------------------------------------------------
695
+ // Endpoint overrides — observer knows prod/daily/sandbox cascade
696
+ // ---------------------------------------------------------------------------
697
+
698
+ export interface EndpointOverrides {
699
+ /** prod: https://cloudcode-pa.googleapis.com */
700
+ prod?: string;
701
+ /** daily: https://daily-cloudcode-pa.googleapis.com */
702
+ daily?: string;
703
+ /** sandbox: https://daily-cloudcode-pa.sandbox.googleapis.com */
704
+ sandbox?: string;
705
+ /** autopush: https://autopush-cloudcode-pa.sandbox.googleapis.com */
706
+ autopush?: string;
707
+ /** quota endpoint override */
708
+ quota?: string;
709
+ /** models endpoint override */
710
+ models?: string;
711
+ /** gemini cli companion endpoint */
712
+ gemini_cli?: string;
713
+ /** cloudaicompanion.googleapis.com */
714
+ cloud_companion?: string;
715
+ /** generateContent base */
716
+ generate?: string;
717
+ /** streamGenerateContent base */
718
+ stream_generate?: string;
719
+ }
720
+
721
+ // ---------------------------------------------------------------------------
722
+ // Quota sub-config
723
+ // ---------------------------------------------------------------------------
724
+
725
+ /**
726
+ * Quota settings block of antigravity.json (v2.1.15 Phase D: renamed from
727
+ * QuotaSettings — quota.js owns that name with the runtime quota shape; this
728
+ * is the user-facing config-file schema).
729
+ */
730
+ export interface QuotaSettings {
731
+ refresh_interval_minutes?: number;
732
+ cache_ttl_minutes?: number;
733
+ soft_threshold?: number;
734
+ hard_threshold?: number;
735
+ dual_pool?: boolean;
736
+ [k: string]: unknown;
737
+ }
738
+
739
+ // ---------------------------------------------------------------------------
740
+ // Bypass metadata — documents how UA / fingerprint bypasses ban
741
+ // ---------------------------------------------------------------------------
742
+
743
+ export interface BypassMetadata {
744
+ /** fallback version when dynamic fetch fails */
745
+ version_fallback?: string;
746
+ /** min supported version */
747
+ min_supported?: string;
748
+ /** custom user agent override — observer warns to keep antigravity/{semver} {os}/{arch} pattern */
749
+ user_agent?: string;
750
+ /** strip x-goog-user-project header to avoid 403 */
751
+ strip_user_project_header?: boolean;
752
+ /** enable fingerprint jitter 0-80ms */
753
+ fingerprint_jitter?: boolean;
754
+ /** pid offset trick for Gemini CLI bypass */
755
+ pid_offset?: boolean;
756
+ }
757
+
758
+ // ---------------------------------------------------------------------------
759
+ // Root config — single union interface covering both field sets (v1 + v2)
760
+ //
761
+ // Reconciliation: the v1 interface required ten flat fields; the v2 interface
762
+ // declared an overlapping but optional superset with nested sub-objects.
763
+ // This merged interface exposes every field from both versions. Fields shared
764
+ // by both versions keep the v2 (superset) type; v1-only fields are optional
765
+ // so both default objects remain assignable.
766
+ // ---------------------------------------------------------------------------
767
+
768
+ export interface AntigravityConfig {
769
+ /** json schema pointer, optional */
770
+ $schema?: string;
771
+ /** config version, integer */
772
+ version?: number;
773
+
774
+ // — core flags shared by v1 and v2 —
775
+ /** try Gemini CLI pool first (v2 default false / v1 default true) */
776
+ cli_first?: boolean;
777
+ /** apply PID offset to avoid automation detection */
778
+ pid_offset_enabled?: boolean;
779
+ /** quota refresh interval in minutes */
780
+ quota_refresh_interval_minutes?: number;
781
+ /** enable Google grounded search (google_search tool) */
782
+ google_search_enabled?: boolean;
783
+ /** debug flag or object (boolean in v1, boolean|object in v2) */
784
+ debug?: DebugConfig;
785
+
786
+ // — extended production fields (v2) —
787
+ /** account rotation strategy */
788
+ rotation_strategy?: RotationStrategy;
789
+ /** alias kept for backward compat with older schema */
790
+ account_rotation?: RotationStrategy;
791
+ /** boolean debug shortcut */
792
+ debug_enabled?: boolean;
793
+ /** full google search object */
794
+ google_search?: GoogleSearchConfig;
795
+ /** endpoint overrides */
796
+ endpoints?: EndpointOverrides;
797
+ /** quota sub-config */
798
+ quota?: QuotaSettings;
799
+ /** bypass metadata */
800
+ bypass?: BypassMetadata;
801
+ /** fallback project id blinded */
802
+ fallback_project_id?: string;
803
+ /** default chat model 2026 */
804
+ default_model?: string;
805
+ /** search/grounding model */
806
+ search_model?: string;
807
+ /** client id override — public client, do not commit secret */
808
+ client_id?: string | null;
809
+ /** client secret override — optional, blinded in logs */
810
+ client_secret?: string | null;
811
+ /** accounts file path override */
812
+ accounts_file?: string | null;
813
+ /** model allowlist custom */
814
+ models_allowlist?: string[];
815
+ /** enable fetch interceptor stripping */
816
+ strip_user_project?: boolean;
817
+
818
+ // — v1-only fields —
819
+ /** toast/notification display scope (v1, default "all") */
820
+ toast_scope?: ToastScope;
821
+ /** quota percentage where soft warning/fallback starts (v1, default 90, range 0-100) */
822
+ soft_quota_threshold_percent?: number;
823
+ /** fallback strategy when quota hits threshold (v1, default "auto") */
824
+ quota_fallback?: QuotaFallback;
825
+ /** soft quota cache TTL in minutes (v1, default 5, range 1-1440) */
826
+ soft_quota_cache_ttl_minutes?: number;
827
+ /** version/update check cache TTL in minutes (v1, default 60, range 1-10080) */
828
+ version_cache_ttl?: number;
829
+
830
+ /** extra */
831
+ [k: string]: unknown;
832
+ }
833
+
834
+ // ---------------------------------------------------------------------------
835
+ // Defaults — library-first deterministic
836
+ // ---------------------------------------------------------------------------
837
+
838
+ export const DEFAULT_ENDPOINTS: Readonly<Required<Pick<EndpointOverrides, "prod" | "daily" | "sandbox" | "autopush">>> =
839
+ {
840
+ prod: "https://cloudcode-pa.googleapis.com",
841
+ daily: "https://daily-cloudcode-pa.googleapis.com",
842
+ sandbox: "https://daily-cloudcode-pa.sandbox.googleapis.com",
843
+ autopush: "https://autopush-cloudcode-pa.sandbox.googleapis.com",
844
+ } as const;
845
+
846
+ export const DEFAULT_QUOTA: Required<QuotaSettings> = {
847
+ refresh_interval_minutes: 15,
848
+ cache_ttl_minutes: 15,
849
+ soft_threshold: 0.9,
850
+ hard_threshold: 1.0,
851
+ dual_pool: true,
852
+ };
853
+
854
+ export const DEFAULT_BYPASS: Required<BypassMetadata> = {
855
+ version_fallback: "1.19.2",
856
+ min_supported: "1.15.8",
857
+ user_agent: "antigravity/1.19.2",
858
+ strip_user_project_header: true,
859
+ fingerprint_jitter: true,
860
+ pid_offset: false,
861
+ };
862
+
863
+ export const DEFAULT_DEBUG_OBJECT: Required<DebugObjectConfig> = {
864
+ enabled: false,
865
+ level: "warn",
866
+ log_file: null,
867
+ retain_days: 7,
868
+ max_bytes: 10 * 1024 * 1024,
869
+ tui_buffer_lines: 1000,
870
+ redact_secrets: true,
871
+ };
872
+
873
+ export const DEFAULT_GOOGLE_SEARCH: Required<GoogleSearchConfig> = {
874
+ enabled: true,
875
+ model: "gemini-3-flash",
876
+ url_context: true,
877
+ include_grounding_metadata: true,
878
+ };
879
+
880
+ export function getDefaultConfig(): AntigravityConfig {
881
+ // observer returns deep clone to avoid mutation between callers
882
+ return {
883
+ $schema: "https://json-schema.org/draft-07/schema#",
884
+ version: 2,
885
+ cli_first: false,
886
+ pid_offset_enabled: false,
887
+ quota_refresh_interval_minutes: 15,
888
+ rotation_strategy: "round-robin",
889
+ account_rotation: "round-robin",
890
+ debug: false,
891
+ debug_enabled: false,
892
+ google_search_enabled: true,
893
+ google_search: { ...DEFAULT_GOOGLE_SEARCH },
894
+ endpoints: { ...DEFAULT_ENDPOINTS },
895
+ quota: { ...DEFAULT_QUOTA },
896
+ bypass: { ...DEFAULT_BYPASS },
897
+ fallback_project_id: "rising-fact-p41fc",
898
+ default_model: "antigravity-gemini-3-flash",
899
+ search_model: "gemini-3-flash",
900
+ client_id: null,
901
+ client_secret: null,
902
+ accounts_file: null,
903
+ models_allowlist: [],
904
+ strip_user_project: true,
905
+ };
906
+ }
907
+
908
+ export const CONFIG_DEFAULTS = getDefaultConfig();
909
+
910
+ /**
911
+ * Immutable v1 default configuration.
912
+ */
913
+ export const DEFAULT_CONFIG: Readonly<AntigravityConfig> = {
914
+ cli_first: true,
915
+ toast_scope: "all",
916
+ soft_quota_threshold_percent: 90,
917
+ quota_fallback: "auto",
918
+ quota_refresh_interval_minutes: 15,
919
+ soft_quota_cache_ttl_minutes: 5,
920
+ pid_offset_enabled: true,
921
+ google_search_enabled: false,
922
+ debug: false,
923
+ version_cache_ttl: 60,
924
+ } as const;
925
+
926
+ // ---------------------------------------------------------------------------
927
+ // JSON Schema Draft-07 — production validations
928
+ // ---------------------------------------------------------------------------
929
+
930
+ /**
931
+ * Schema for ~/.config/opencode/antigravity.json
932
+ * The observer documents this file with third-person view and bilingual help.
933
+ */
934
+ export const ANTIGRAVITY_CONFIG_JSON_SCHEMA = {
935
+ $schema: "http://json-schema.org/draft-07/schema#",
936
+ $id: "https://opencode.ai/schemas/antigravity.json",
937
+ title: "devthink provider config — antigravity.json",
938
+ description:
939
+ "Configuration for the devthink provider plugin. Bypass metadata: mimics antigravity/{ver} {os}/{arch} User-Agent, strips x-goog-user-project, endpoint cascade 403/404/5xx, supports Gemini CLI dual quota pool, round-robin/sticky rotation, google_search grounding.",
940
+ type: "object",
941
+ additionalProperties: false,
942
+ properties: {
943
+ $schema: {
944
+ type: "string",
945
+ format: "uri",
946
+ description: "JSON schema pointer",
947
+ default: "https://json-schema.org/draft-07/schema#",
948
+ examples: ["https://json-schema.org/draft-07/schema#", "./antigravity.schema.json"],
949
+ },
950
+ version: {
951
+ type: "integer",
952
+ minimum: 1,
953
+ maximum: 10,
954
+ default: 2,
955
+ description: "config version",
956
+ examples: [2],
957
+ },
958
+ cli_first: {
959
+ type: "boolean",
960
+ default: false,
961
+ description:
962
+ "When true, tries Gemini CLI quota pool first before Antigravity pool. Useful when Antigravity quota is exhausted but CLI pool still has tokens.",
963
+ examples: [false, true],
964
+ },
965
+ pid_offset_enabled: {
966
+ type: "boolean",
967
+ default: false,
968
+ description:
969
+ "Enables PID offset trick to bypass per-PID rate limiting observed in Gemini CLI gateway. When enabled, adds pid % 1000 to jitter.",
970
+ examples: [false, true],
971
+ },
972
+ quota_refresh_interval_minutes: {
973
+ type: "integer",
974
+ minimum: 1,
975
+ maximum: 120,
976
+ default: 15,
977
+ description:
978
+ "Interval in minutes for background quota refresh. Adaptive TTL 2-10min when near soft threshold 90%.",
979
+ examples: [5, 15, 30],
980
+ },
981
+ rotation_strategy: {
982
+ type: "string",
983
+ enum: ROTATION_STRATEGIES as unknown as string[],
984
+ default: "round-robin",
985
+ description:
986
+ "Multi-account rotation strategy — round-robin distributes evenly, sticky keeps same account until quota exhausted.",
987
+ examples: ["round-robin", "sticky"],
988
+ },
989
+ account_rotation: {
990
+ type: "string",
991
+ enum: ROTATION_STRATEGIES as unknown as string[],
992
+ default: "round-robin",
993
+ description: "Alias for rotation_strategy (backward compat)",
994
+ examples: ["round-robin", "sticky"],
995
+ },
996
+ debug: {
997
+ oneOf: [
998
+ { type: "boolean" },
999
+ {
1000
+ type: "object",
1001
+ additionalProperties: false,
1002
+ properties: {
1003
+ enabled: {
1004
+ type: "boolean",
1005
+ default: false,
1006
+ description: "enable debug",
1007
+ examples: [false, true],
1008
+ },
1009
+ level: {
1010
+ type: "string",
1011
+ enum: ["debug", "info", "warn", "error", "silent"],
1012
+ default: "warn",
1013
+ description: "log level",
1014
+ examples: ["debug", "warn"],
1015
+ },
1016
+ log_file: {
1017
+ type: ["string", "null"],
1018
+ default: null,
1019
+ description: "custom log file path",
1020
+ examples: [null, "~/.config/opencode/antigravity-debug.log"],
1021
+ },
1022
+ retain_days: {
1023
+ type: "integer",
1024
+ minimum: 1,
1025
+ maximum: 90,
1026
+ default: 7,
1027
+ description: "log retention days",
1028
+ examples: [7],
1029
+ },
1030
+ max_bytes: {
1031
+ type: "integer",
1032
+ minimum: 1024,
1033
+ maximum: 100 * 1024 * 1024,
1034
+ default: 10485760,
1035
+ description: "max bytes before rotation, 10MB default",
1036
+ examples: [10485760],
1037
+ },
1038
+ tui_buffer_lines: {
1039
+ type: "integer",
1040
+ minimum: 10,
1041
+ maximum: 10000,
1042
+ default: 1000,
1043
+ description: "circular TUI buffer lines",
1044
+ examples: [1000],
1045
+ },
1046
+ redact_secrets: {
1047
+ type: "boolean",
1048
+ default: true,
1049
+ description: "redact secrets in logs",
1050
+ examples: [true],
1051
+ },
1052
+ },
1053
+ },
1054
+ ],
1055
+ default: false,
1056
+ description: "Debug mode — boolean or object with rotation, retention, redaction.",
1057
+ examples: [false, true, { enabled: true, level: "debug" }],
1058
+ },
1059
+ debug_enabled: {
1060
+ type: "boolean",
1061
+ default: false,
1062
+ description: "shortcut for debug.enabled",
1063
+ examples: [false, true],
1064
+ },
1065
+ google_search_enabled: {
1066
+ type: "boolean",
1067
+ default: true,
1068
+ description: "Enable native google_search grounding tool (Gemini) injection.",
1069
+ examples: [true, false],
1070
+ },
1071
+ google_search: {
1072
+ type: "object",
1073
+ additionalProperties: false,
1074
+ default: { ...DEFAULT_GOOGLE_SEARCH },
1075
+ description: "Google search grounding full config",
1076
+ properties: {
1077
+ enabled: {
1078
+ type: "boolean",
1079
+ default: true,
1080
+ description: "enable google_search tool",
1081
+ examples: [true, false],
1082
+ },
1083
+ model: {
1084
+ type: "string",
1085
+ default: "gemini-3-flash",
1086
+ description: "model used for search grounding",
1087
+ examples: ["gemini-3-flash", "gemini-3-flash-preview"],
1088
+ },
1089
+ url_context: {
1090
+ type: "boolean",
1091
+ default: true,
1092
+ description: "enable url_context companion tool",
1093
+ examples: [true, false],
1094
+ },
1095
+ include_grounding_metadata: {
1096
+ type: "boolean",
1097
+ default: true,
1098
+ description: "include groundingMetadata in response",
1099
+ examples: [true],
1100
+ },
1101
+ },
1102
+ },
1103
+ endpoints: {
1104
+ type: "object",
1105
+ additionalProperties: false,
1106
+ description: "Override Antigravity endpoints. Observer warns: must use https and googleapis.com for bypass.",
1107
+ properties: {
1108
+ prod: {
1109
+ type: "string",
1110
+ format: "uri",
1111
+ default: "https://cloudcode-pa.googleapis.com",
1112
+ description: "PROD endpoint",
1113
+ examples: ["https://cloudcode-pa.googleapis.com"],
1114
+ },
1115
+ daily: {
1116
+ type: "string",
1117
+ format: "uri",
1118
+ default: "https://daily-cloudcode-pa.googleapis.com",
1119
+ description: "DAILY endpoint",
1120
+ examples: ["https://daily-cloudcode-pa.googleapis.com"],
1121
+ },
1122
+ sandbox: {
1123
+ type: "string",
1124
+ format: "uri",
1125
+ default: "https://daily-cloudcode-pa.sandbox.googleapis.com",
1126
+ description: "SANDBOX endpoint",
1127
+ examples: ["https://daily-cloudcode-pa.sandbox.googleapis.com"],
1128
+ },
1129
+ autopush: {
1130
+ type: "string",
1131
+ format: "uri",
1132
+ default: "https://autopush-cloudcode-pa.sandbox.googleapis.com",
1133
+ description: "AUTOPUSH optional",
1134
+ examples: ["https://autopush-cloudcode-pa.sandbox.googleapis.com"],
1135
+ },
1136
+ quota: {
1137
+ type: "string",
1138
+ format: "uri",
1139
+ description: "quota endpoint override",
1140
+ examples: ["https://cloudcode-pa.googleapis.com"],
1141
+ },
1142
+ models: {
1143
+ type: "string",
1144
+ format: "uri",
1145
+ description: "models endpoint override",
1146
+ examples: ["https://cloudcode-pa.googleapis.com"],
1147
+ },
1148
+ gemini_cli: {
1149
+ type: "string",
1150
+ format: "uri",
1151
+ description: "Gemini CLI companion endpoint",
1152
+ examples: ["https://cloudaicompanion.googleapis.com"],
1153
+ },
1154
+ cloud_companion: {
1155
+ type: "string",
1156
+ format: "uri",
1157
+ description: "cloudaicompanion.googleapis.com",
1158
+ examples: ["https://cloudaicompanion.googleapis.com"],
1159
+ },
1160
+ generate: {
1161
+ type: "string",
1162
+ format: "uri",
1163
+ description: "generateContent base",
1164
+ examples: ["https://cloudcode-pa.googleapis.com"],
1165
+ },
1166
+ stream_generate: {
1167
+ type: "string",
1168
+ format: "uri",
1169
+ description: "streamGenerateContent base",
1170
+ examples: ["https://cloudcode-pa.googleapis.com"],
1171
+ },
1172
+ },
1173
+ },
1174
+ quota: {
1175
+ type: "object",
1176
+ additionalProperties: false,
1177
+ description: "Quota config",
1178
+ properties: {
1179
+ refresh_interval_minutes: {
1180
+ type: "integer",
1181
+ minimum: 1,
1182
+ maximum: 120,
1183
+ default: 15,
1184
+ examples: [15],
1185
+ },
1186
+ cache_ttl_minutes: {
1187
+ type: "integer",
1188
+ minimum: 1,
1189
+ maximum: 60,
1190
+ default: 15,
1191
+ examples: [15],
1192
+ },
1193
+ soft_threshold: {
1194
+ type: "number",
1195
+ minimum: 0.1,
1196
+ maximum: 1.0,
1197
+ default: 0.9,
1198
+ examples: [0.9],
1199
+ },
1200
+ hard_threshold: {
1201
+ type: "number",
1202
+ minimum: 0.5,
1203
+ maximum: 1.0,
1204
+ default: 1.0,
1205
+ examples: [1.0],
1206
+ },
1207
+ dual_pool: {
1208
+ type: "boolean",
1209
+ default: true,
1210
+ description: "enable dual pool Antigravity + Gemini CLI",
1211
+ examples: [true],
1212
+ },
1213
+ },
1214
+ },
1215
+ bypass: {
1216
+ type: "object",
1217
+ additionalProperties: false,
1218
+ description:
1219
+ "Bypass metadata — documents how library imitates Antigravity traffic. UA must match antigravity/{semver} {os}/{arch}.",
1220
+ properties: {
1221
+ version_fallback: {
1222
+ type: "string",
1223
+ pattern: "^\\d+\\.\\d+\\.\\d+",
1224
+ default: "1.19.2",
1225
+ examples: ["1.19.2"],
1226
+ },
1227
+ min_supported: {
1228
+ type: "string",
1229
+ pattern: "^\\d+\\.\\d+\\.\\d+",
1230
+ default: "1.15.8",
1231
+ examples: ["1.15.8"],
1232
+ },
1233
+ user_agent: {
1234
+ type: "string",
1235
+ default: "antigravity/1.19.2",
1236
+ description: "full UA, should contain antigravity/ prefix",
1237
+ examples: ["antigravity/1.19.2 darwin/arm64", "antigravity/1.19.2 linux/x64"],
1238
+ },
1239
+ strip_user_project_header: {
1240
+ type: "boolean",
1241
+ default: true,
1242
+ description: "strip x-goog-user-project to avoid 403",
1243
+ examples: [true],
1244
+ },
1245
+ fingerprint_jitter: {
1246
+ type: "boolean",
1247
+ default: true,
1248
+ examples: [true],
1249
+ },
1250
+ pid_offset: {
1251
+ type: "boolean",
1252
+ default: false,
1253
+ examples: [false, true],
1254
+ },
1255
+ },
1256
+ },
1257
+ fallback_project_id: {
1258
+ type: "string",
1259
+ minLength: 3,
1260
+ default: "rising-fact-p41fc",
1261
+ description: "fallback project id blinded",
1262
+ examples: ["rising-fact-p41fc"],
1263
+ },
1264
+ default_model: {
1265
+ type: "string",
1266
+ default: "antigravity-gemini-3-flash",
1267
+ description: "default chat model 2026",
1268
+ examples: ["antigravity-gemini-3-flash", "gemini-3-pro-preview"],
1269
+ },
1270
+ search_model: {
1271
+ type: "string",
1272
+ default: "gemini-3-flash",
1273
+ description: "search grounding model",
1274
+ examples: ["gemini-3-flash", "gemini-3-flash-preview"],
1275
+ },
1276
+ client_id: {
1277
+ type: ["string", "null"],
1278
+ default: null,
1279
+ description: "OAuth client_id override (public)",
1280
+ examples: [null, "1071006060591-...apps.googleusercontent.com"],
1281
+ },
1282
+ client_secret: {
1283
+ type: ["string", "null"],
1284
+ default: null,
1285
+ description: "OAuth client_secret override — do not commit",
1286
+ examples: [null],
1287
+ },
1288
+ accounts_file: {
1289
+ type: ["string", "null"],
1290
+ default: null,
1291
+ format: "uri-reference",
1292
+ description: "custom accounts file path",
1293
+ examples: [null, "~/.config/opencode/antigravity-accounts.json"],
1294
+ },
1295
+ models_allowlist: {
1296
+ type: "array",
1297
+ items: { type: "string" },
1298
+ default: [],
1299
+ description: "optional model allowlist",
1300
+ examples: [[], ["antigravity-gemini-3-flash", "claude-opus-4-6-thinking"]],
1301
+ },
1302
+ strip_user_project: {
1303
+ type: "boolean",
1304
+ default: true,
1305
+ description: "legacy alias for bypass.strip_user_project_header",
1306
+ examples: [true],
1307
+ },
1308
+ toast_scope: {
1309
+ type: "string",
1310
+ enum: ["all", "project", "minimal", "none"],
1311
+ default: "all",
1312
+ description: "toast/notification display scope (v1 field)",
1313
+ examples: ["all", "project", "minimal", "none"],
1314
+ },
1315
+ soft_quota_threshold_percent: {
1316
+ type: "number",
1317
+ minimum: 0,
1318
+ maximum: 100,
1319
+ default: 90,
1320
+ description: "quota percentage where soft warning/fallback starts (v1 field)",
1321
+ examples: [90],
1322
+ },
1323
+ quota_fallback: {
1324
+ oneOf: [{ type: "boolean" }, { type: "string" }],
1325
+ default: "auto",
1326
+ description: 'fallback strategy on quota exhaustion: boolean | "auto" | modelName (v1 field)',
1327
+ examples: ["auto", true, false],
1328
+ },
1329
+ soft_quota_cache_ttl_minutes: {
1330
+ type: "integer",
1331
+ minimum: 1,
1332
+ maximum: 1440,
1333
+ default: 5,
1334
+ description: "soft quota cache TTL in minutes (v1 field)",
1335
+ examples: [5],
1336
+ },
1337
+ version_cache_ttl: {
1338
+ type: "integer",
1339
+ minimum: 1,
1340
+ maximum: 10080,
1341
+ default: 60,
1342
+ description: "version/update check cache TTL in minutes (v1 field)",
1343
+ examples: [60],
1344
+ },
1345
+ },
1346
+ required: [],
1347
+ } as const;
1348
+
1349
+ // ---------------------------------------------------------------------------
1350
+ // Path resolution — third-person, no side effects here
1351
+ // ---------------------------------------------------------------------------
1352
+
1353
+ /**
1354
+ * The observer resolves config directory honoring OPENCODE_CONFIG_DIR.
1355
+ */
1356
+ export function resolveConfigDir(): string {
1357
+ const env = process.env.OPENCODE_CONFIG_DIR?.trim();
1358
+ if (env && env.length > 0) return path.resolve(env);
1359
+ return path.join(os.homedir(), ".config", "opencode");
1360
+ }
1361
+
1362
+ /**
1363
+ * Resolves full antigravity.json path.
1364
+ * @param customPath - optional explicit path from caller
1365
+ */
1366
+ export function resolveConfigPath(customPath?: string): string {
1367
+ if (customPath && customPath.trim().length > 0) {
1368
+ return path.resolve(customPath.trim());
1369
+ }
1370
+ return path.join(resolveConfigDir(), CONFIG_FILE_NAME);
1371
+ }
1372
+
1373
+ /**
1374
+ * Returns the absolute config file path for the current OS:
1375
+ * ~/.config/opencode/antigravity.json.
1376
+ * Honors XDG_CONFIG_HOME on Linux when defined.
1377
+ *
1378
+ * @returns absolute path
1379
+ */
1380
+ export function getConfigPath(): string {
1381
+ // XDG override (Linux standard)
1382
+ const xdg = process.env.XDG_CONFIG_HOME;
1383
+ if (xdg && xdg.trim().length > 0) {
1384
+ return path.join(xdg, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
1385
+ }
1386
+
1387
+ const home = os.homedir();
1388
+
1389
+ // Robust fallback if homedir fails (rare)
1390
+ if (!home || home.trim().length === 0) {
1391
+ // try HOME env
1392
+ const envHome = process.env.HOME || process.env.USERPROFILE || "";
1393
+ if (envHome) {
1394
+ return path.join(envHome, ".config", CONFIG_DIR_NAME, CONFIG_FILE_NAME);
1395
+ }
1396
+ // last-resort relative fallback (not ideal but avoids throwing)
1397
+ return path.join(process.cwd(), ".config", CONFIG_DIR_NAME, CONFIG_FILE_NAME);
1398
+ }
1399
+
1400
+ return path.join(home, ".config", CONFIG_DIR_NAME, CONFIG_FILE_NAME);
1401
+ }
1402
+
1403
+ /**
1404
+ * Ensures directory exists, idempotent, 0755.
1405
+ */
1406
+ async function ensureDir(dir: string): Promise<void> {
1407
+ try {
1408
+ await fsp.mkdir(dir, { recursive: true, mode: DIR_MODE });
1409
+ // chmod best-effort to 0755 even if existed
1410
+ try {
1411
+ await fsp.chmod(dir, DIR_MODE);
1412
+ } catch {
1413
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1414
+ }
1415
+ } catch (err: any) {
1416
+ // if mkdir fails because exists as file, throw production error
1417
+ if (err?.code !== "EEXIST") throw err;
1418
+ }
1419
+ }
1420
+
1421
+ /** Ensures the config directory exists (mkdir -p). */
1422
+ function ensureConfigDirExists(configPath: string): void {
1423
+ const dir = path.dirname(configPath);
1424
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
1425
+ }
1426
+
1427
+ // ---------------------------------------------------------------------------
1428
+ // Atomic write — tmp + chmod 600 + rename (async v2 / sync v1)
1429
+ // ---------------------------------------------------------------------------
1430
+
1431
+ /**
1432
+ * Atomic write helper (async).
1433
+ * The observer writes to a temp sibling file then renames, to avoid partial reads.
1434
+ * @param targetPath - final destination
1435
+ * @param content - string content to write
1436
+ */
1437
+ async function atomicWriteFileAtomic(targetPath: string, content: string): Promise<void> {
1438
+ const dir = path.dirname(targetPath);
1439
+ await ensureDir(dir);
1440
+ const random = crypto.randomBytes(6).toString("hex");
1441
+ const tmp = path.join(dir, `.${path.basename(targetPath)}.${process.pid}.${random}.tmp`);
1442
+
1443
+ try {
1444
+ await fsp.writeFile(tmp, content, { encoding: "utf8", mode: FILE_MODE });
1445
+ try {
1446
+ await fsp.chmod(tmp, FILE_MODE);
1447
+ } catch {
1448
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1449
+ }
1450
+ // fsync dir? best-effort not critical for this config — production tolerant
1451
+ await fsp.rename(tmp, targetPath);
1452
+ // final chmod on target for existing file overwrite case
1453
+ try {
1454
+ await fsp.chmod(targetPath, FILE_MODE);
1455
+ } catch {
1456
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1457
+ }
1458
+ } finally {
1459
+ // cleanup tmp if rename failed
1460
+ try {
1461
+ await fsp.unlink(tmp);
1462
+ } catch {
1463
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ /**
1469
+ * Atomic sync write with chmod 0o600.
1470
+ * - Writes to a temp file in the same dir
1471
+ * - chmod 0o600
1472
+ * - atomic rename
1473
+ *
1474
+ * @param filePath final destination
1475
+ * @param data JSON string
1476
+ */
1477
+ function atomicWriteFile(filePath: string, data: string): void {
1478
+ ensureConfigDirExists(filePath);
1479
+
1480
+ const dir = path.dirname(filePath);
1481
+ const tmpName = `${path.basename(filePath)}.tmp.${process.pid}.${Date.now()}.${Math.random()
1482
+ .toString(36)
1483
+ .slice(2, 8)}`;
1484
+ const tmpPath = path.join(dir, tmpName);
1485
+
1486
+ try {
1487
+ // write temp
1488
+ fs.writeFileSync(tmpPath, data, { encoding: "utf8", mode: 0o600 });
1489
+
1490
+ // guarantee 0o600 even if umask interfered
1491
+ try {
1492
+ fs.chmodSync(tmpPath, 0o600);
1493
+ } catch {
1494
+ // chmod may fail on Windows; ignore but continue
1495
+ }
1496
+
1497
+ // fsync the file for durability (best-effort)
1498
+ try {
1499
+ const fd = fs.openSync(tmpPath, "r");
1500
+ try {
1501
+ fs.fsyncSync(fd);
1502
+ } finally {
1503
+ fs.closeSync(fd);
1504
+ }
1505
+ } catch {
1506
+ // fsync optional
1507
+ }
1508
+
1509
+ // atomic rename
1510
+ fs.renameSync(tmpPath, filePath);
1511
+
1512
+ // guarantee 0o600 on the final destination too
1513
+ try {
1514
+ fs.chmodSync(filePath, 0o600);
1515
+ } catch {
1516
+ // ignore on Windows
1517
+ }
1518
+ } finally {
1519
+ // cleanup temp if still present
1520
+ try {
1521
+ if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
1522
+ } catch {
1523
+ // ignore
1524
+ }
1525
+ }
1526
+ }
1527
+
1528
+ // ---------------------------------------------------------------------------
1529
+ // Shared helpers — url validation, deep merge, redaction, primitive guards
1530
+ // ---------------------------------------------------------------------------
1531
+
1532
+ function isValidUri(u: string): boolean {
1533
+ try {
1534
+ const url = new URL(u);
1535
+ return url.protocol === "https:" || url.protocol === "http:";
1536
+ } catch {
1537
+ return false;
1538
+ }
1539
+ }
1540
+
1541
+ function isObject(v: unknown): v is Record<string, unknown> {
1542
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1543
+ }
1544
+
1545
+ function isBoolean(v: unknown): v is boolean {
1546
+ return typeof v === "boolean";
1547
+ }
1548
+
1549
+ function isNumber(v: unknown): v is number {
1550
+ return typeof v === "number" && Number.isFinite(v);
1551
+ }
1552
+
1553
+ function isString(v: unknown): v is string {
1554
+ return typeof v === "string";
1555
+ }
1556
+
1557
+ function isValidToastScope(v: unknown): v is ToastScope {
1558
+ return v === "all" || v === "project" || v === "minimal" || v === "none";
1559
+ }
1560
+
1561
+ function isValidQuotaFallback(v: unknown): v is QuotaFallback {
1562
+ if (isBoolean(v)) return true;
1563
+ if (v === "auto") return true;
1564
+ if (isString(v)) {
1565
+ // accepts any non-empty string (model name) or known model
1566
+ return v.trim().length > 0;
1567
+ }
1568
+ return false;
1569
+ }
1570
+
1571
+ /**
1572
+ * Deep merge defaults <- override; observer keeps primitives from override when defined.
1573
+ */
1574
+ function deepMerge<T extends Record<string, any>>(defaults: T, override: Partial<T>): T {
1575
+ const out: any = { ...defaults };
1576
+ for (const k of Object.keys(override)) {
1577
+ const ov = (override as any)[k];
1578
+ if (ov === undefined) continue;
1579
+ const dv = (defaults as any)[k];
1580
+ if (isObject(dv) && isObject(ov) && !Array.isArray(dv) && !Array.isArray(ov)) {
1581
+ out[k] = deepMerge(dv as any, ov as any);
1582
+ } else {
1583
+ out[k] = ov;
1584
+ }
1585
+ }
1586
+ return out as T;
1587
+ }
1588
+
1589
+ function redactedCopy(cfg: AntigravityConfig): AntigravityConfig {
1590
+ const c: any = { ...cfg };
1591
+ if (c.client_secret) c.client_secret = "***REDACTED***";
1592
+ return c;
1593
+ }
1594
+
1595
+ // ---------------------------------------------------------------------------
1596
+ // Validation errors — unified class accepting both constructor signatures
1597
+ // ---------------------------------------------------------------------------
1598
+
1599
+ export interface ValidationError {
1600
+ path: string;
1601
+ message: string;
1602
+ value?: unknown;
1603
+ }
1604
+
1605
+ /**
1606
+ * Unified config validation error.
1607
+ * - v2 signature: new ConfigValidationError(message, structuredErrors[])
1608
+ * - v1 signature: new ConfigValidationError(stringErrorList)
1609
+ */
1610
+ export class ConfigValidationError extends Error {
1611
+ public readonly errors: ReadonlyArray<ValidationError | string>;
1612
+ constructor(messageOrErrors: string | ReadonlyArray<string>, errors?: ReadonlyArray<ValidationError>) {
1613
+ if (Array.isArray(messageOrErrors)) {
1614
+ // v1 style: array of message strings
1615
+ super(`Invalid antigravity config: ${(messageOrErrors as ReadonlyArray<string>).join("; ")}`);
1616
+ this.name = "ConfigValidationError";
1617
+ this.errors = messageOrErrors as ReadonlyArray<ValidationError | string>;
1618
+ } else {
1619
+ // v2 style: message + structured errors
1620
+ super(messageOrErrors as string);
1621
+ this.name = "ConfigValidationError";
1622
+ this.errors = (errors ?? []) as ReadonlyArray<ValidationError | string>;
1623
+ }
1624
+ }
1625
+ }
1626
+
1627
+ // ---------------------------------------------------------------------------
1628
+ // Validation — production-ready, no placeholders (v2 strict validator)
1629
+ // ---------------------------------------------------------------------------
1630
+
1631
+ /**
1632
+ * Validates config strictly for production.
1633
+ * The observer collects all errors before throwing, to give full feedback.
1634
+ *
1635
+ * @param input - raw parsed json
1636
+ * @returns sanitized AntigravityConfig
1637
+ * @throws ConfigValidationError when invalid
1638
+ */
1639
+ export function validate(input: unknown): AntigravityConfig {
1640
+ const result = validateInternal(input);
1641
+ if (!result.valid) {
1642
+ throw new ConfigValidationError(
1643
+ `config validation failed: ${result.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`,
1644
+ result.errors,
1645
+ );
1646
+ }
1647
+ return result.config!;
1648
+ }
1649
+
1650
+ export function validateConfig(input: unknown): {
1651
+ valid: boolean;
1652
+ errors: ValidationError[];
1653
+ config?: AntigravityConfig | undefined;
1654
+ } {
1655
+ return validateInternal(input);
1656
+ }
1657
+
1658
+ function validateInternal(input: unknown): {
1659
+ valid: boolean;
1660
+ errors: ValidationError[];
1661
+ config?: AntigravityConfig | undefined;
1662
+ } {
1663
+ const errors: ValidationError[] = [];
1664
+ const defaults = getDefaultConfig();
1665
+
1666
+ if (!isObject(input)) {
1667
+ errors.push({ path: "$", message: "must be object", value: input });
1668
+ return { valid: false, errors };
1669
+ }
1670
+
1671
+ const raw = input as Record<string, unknown>;
1672
+ const out: AntigravityConfig = { ...defaults } as any;
1673
+
1674
+ // helper to push error
1675
+ const err = (p: string, msg: string, v?: unknown) => errors.push({ path: p, message: msg, value: v });
1676
+
1677
+ // version
1678
+ if (raw.version !== undefined) {
1679
+ if (!Number.isInteger(raw.version) || (raw.version as number) < 1 || (raw.version as number) > 10) {
1680
+ err("version", "must be integer 1-10", raw.version);
1681
+ } else {
1682
+ out.version = raw.version as number;
1683
+ }
1684
+ }
1685
+
1686
+ // cli_first boolean — required by prompt
1687
+ if (raw.cli_first !== undefined) {
1688
+ if (typeof raw.cli_first !== "boolean") err("cli_first", "must be boolean", raw.cli_first);
1689
+ else out.cli_first = raw.cli_first;
1690
+ }
1691
+
1692
+ // pid_offset_enabled boolean — required
1693
+ if (raw.pid_offset_enabled !== undefined) {
1694
+ if (typeof raw.pid_offset_enabled !== "boolean")
1695
+ err("pid_offset_enabled", "must be boolean", raw.pid_offset_enabled);
1696
+ else out.pid_offset_enabled = raw.pid_offset_enabled as boolean;
1697
+ }
1698
+
1699
+ // quota_refresh_interval_minutes integer 1-120
1700
+ if (raw.quota_refresh_interval_minutes !== undefined) {
1701
+ if (
1702
+ !Number.isInteger(raw.quota_refresh_interval_minutes) ||
1703
+ (raw.quota_refresh_interval_minutes as number) < 1 ||
1704
+ (raw.quota_refresh_interval_minutes as number) > 120
1705
+ ) {
1706
+ err("quota_refresh_interval_minutes", "must be integer 1-120", raw.quota_refresh_interval_minutes);
1707
+ } else {
1708
+ out.quota_refresh_interval_minutes = raw.quota_refresh_interval_minutes as number;
1709
+ }
1710
+ }
1711
+
1712
+ // rotation_strategy enum
1713
+ const checkRotation = (val: unknown, p: string): RotationStrategy | null => {
1714
+ if (val === undefined) return null;
1715
+ if (typeof val !== "string" || !(ROTATION_STRATEGIES as readonly string[]).includes(val)) {
1716
+ err(p, `must be one of ${ROTATION_STRATEGIES.join(", ")}`, val);
1717
+ return null;
1718
+ }
1719
+ return val as RotationStrategy;
1720
+ };
1721
+ const rs = checkRotation(raw.rotation_strategy, "rotation_strategy");
1722
+ if (rs) {
1723
+ out.rotation_strategy = rs;
1724
+ out.account_rotation = rs; // keep alias in sync
1725
+ }
1726
+ const ar = checkRotation(raw.account_rotation, "account_rotation");
1727
+ if (ar) {
1728
+ out.account_rotation = ar;
1729
+ // if rotation_strategy not set, mirror
1730
+ if (raw.rotation_strategy === undefined) out.rotation_strategy = ar;
1731
+ }
1732
+
1733
+ // debug boolean | object
1734
+ if (raw.debug !== undefined) {
1735
+ const d = raw.debug;
1736
+ if (typeof d === "boolean") {
1737
+ out.debug = d;
1738
+ } else if (isObject(d)) {
1739
+ const obj: DebugObjectConfig = {};
1740
+ const dRec = d as Record<string, unknown>;
1741
+ if (dRec.enabled !== undefined) {
1742
+ if (typeof dRec.enabled !== "boolean") err("debug.enabled", "must be boolean", dRec.enabled);
1743
+ else obj.enabled = dRec.enabled;
1744
+ }
1745
+ if (dRec.level !== undefined) {
1746
+ const levels = ["debug", "info", "warn", "error", "silent"];
1747
+ if (typeof dRec.level !== "string" || !levels.includes(dRec.level))
1748
+ err("debug.level", `must be ${levels.join("|")}`, dRec.level);
1749
+ else obj.level = dRec.level as DebugLevel;
1750
+ }
1751
+ if (dRec.log_file !== undefined) {
1752
+ if (dRec.log_file !== null && typeof dRec.log_file !== "string")
1753
+ err("debug.log_file", "must be string|null", dRec.log_file);
1754
+ else obj.log_file = dRec.log_file as string | null;
1755
+ }
1756
+ if (dRec.retain_days !== undefined) {
1757
+ if (
1758
+ !Number.isInteger(dRec.retain_days) ||
1759
+ (dRec.retain_days as number) < 1 ||
1760
+ (dRec.retain_days as number) > 90
1761
+ )
1762
+ err("debug.retain_days", "must be integer 1-90", dRec.retain_days);
1763
+ else obj.retain_days = dRec.retain_days as number;
1764
+ }
1765
+ if (dRec.max_bytes !== undefined) {
1766
+ if (
1767
+ !Number.isInteger(dRec.max_bytes) ||
1768
+ (dRec.max_bytes as number) < 1024 ||
1769
+ (dRec.max_bytes as number) > 100 * 1024 * 1024
1770
+ )
1771
+ err("debug.max_bytes", "must be 1024-104857600", dRec.max_bytes);
1772
+ else obj.max_bytes = dRec.max_bytes as number;
1773
+ }
1774
+ if (dRec.tui_buffer_lines !== undefined) {
1775
+ if (
1776
+ !Number.isInteger(dRec.tui_buffer_lines) ||
1777
+ (dRec.tui_buffer_lines as number) < 10 ||
1778
+ (dRec.tui_buffer_lines as number) > 10000
1779
+ )
1780
+ err("debug.tui_buffer_lines", "must be 10-10000", dRec.tui_buffer_lines);
1781
+ else obj.tui_buffer_lines = dRec.tui_buffer_lines as number;
1782
+ }
1783
+ if (dRec.redact_secrets !== undefined) {
1784
+ if (typeof dRec.redact_secrets !== "boolean")
1785
+ err("debug.redact_secrets", "must be boolean", dRec.redact_secrets);
1786
+ else obj.redact_secrets = dRec.redact_secrets;
1787
+ }
1788
+ // check unknown keys inside debug
1789
+ const allowedDebugKeys = [
1790
+ "enabled",
1791
+ "level",
1792
+ "log_file",
1793
+ "retain_days",
1794
+ "max_bytes",
1795
+ "tui_buffer_lines",
1796
+ "redact_secrets",
1797
+ ];
1798
+ for (const k of Object.keys(dRec)) {
1799
+ if (!allowedDebugKeys.includes(k)) err(`debug.${k}`, "unknown property", dRec[k]);
1800
+ }
1801
+ out.debug = deepMerge(DEFAULT_DEBUG_OBJECT as any, obj as any);
1802
+ } else {
1803
+ err("debug", "must be boolean or object", d);
1804
+ }
1805
+ }
1806
+
1807
+ if (raw.debug_enabled !== undefined) {
1808
+ if (typeof raw.debug_enabled !== "boolean") err("debug_enabled", "must be boolean", raw.debug_enabled);
1809
+ else out.debug_enabled = raw.debug_enabled;
1810
+ }
1811
+
1812
+ // google_search_enabled boolean
1813
+ if (raw.google_search_enabled !== undefined) {
1814
+ if (typeof raw.google_search_enabled !== "boolean")
1815
+ err("google_search_enabled", "must be boolean", raw.google_search_enabled);
1816
+ else out.google_search_enabled = raw.google_search_enabled;
1817
+ }
1818
+
1819
+ if (raw.google_search !== undefined) {
1820
+ if (!isObject(raw.google_search)) {
1821
+ err("google_search", "must be object", raw.google_search);
1822
+ } else {
1823
+ const gsRaw = raw.google_search as Record<string, unknown>;
1824
+ const gs: GoogleSearchConfig = {};
1825
+ if (gsRaw.enabled !== undefined) {
1826
+ if (typeof gsRaw.enabled !== "boolean") err("google_search.enabled", "must be boolean", gsRaw.enabled);
1827
+ else gs.enabled = gsRaw.enabled;
1828
+ }
1829
+ if (gsRaw.model !== undefined) {
1830
+ if (typeof gsRaw.model !== "string" || (gsRaw.model as string).length < 2)
1831
+ err("google_search.model", "must be non-empty string", gsRaw.model);
1832
+ else gs.model = gsRaw.model;
1833
+ }
1834
+ if (gsRaw.url_context !== undefined) {
1835
+ if (typeof gsRaw.url_context !== "boolean")
1836
+ err("google_search.url_context", "must be boolean", gsRaw.url_context);
1837
+ else gs.url_context = gsRaw.url_context;
1838
+ }
1839
+ if (gsRaw.include_grounding_metadata !== undefined) {
1840
+ if (typeof gsRaw.include_grounding_metadata !== "boolean")
1841
+ err("google_search.include_grounding_metadata", "must be boolean", gsRaw.include_grounding_metadata);
1842
+ else gs.include_grounding_metadata = gsRaw.include_grounding_metadata;
1843
+ }
1844
+ for (const k of Object.keys(gsRaw)) {
1845
+ if (!["enabled", "model", "url_context", "include_grounding_metadata"].includes(k))
1846
+ err(`google_search.${k}`, "unknown property", gsRaw[k]);
1847
+ }
1848
+ out.google_search = deepMerge(DEFAULT_GOOGLE_SEARCH as any, gs as any);
1849
+ }
1850
+ }
1851
+
1852
+ // sync shortcut booleans with objects
1853
+ if (out.google_search_enabled !== undefined && out.google_search) {
1854
+ // if shortcut false, enforce object disabled
1855
+ if (out.google_search_enabled === false) {
1856
+ out.google_search.enabled = false;
1857
+ }
1858
+ if (out.google_search.enabled === false && out.google_search_enabled === true) {
1859
+ // object takes precedence, but mirror to shortcut for consistency
1860
+ out.google_search_enabled = false;
1861
+ }
1862
+ }
1863
+ if (out.debug !== undefined) {
1864
+ if (typeof out.debug === "boolean") {
1865
+ out.debug_enabled = out.debug;
1866
+ } else if (isObject(out.debug)) {
1867
+ out.debug_enabled = (out.debug as DebugObjectConfig).enabled ?? false;
1868
+ }
1869
+ }
1870
+
1871
+ // endpoints overrides uri validation
1872
+ if (raw.endpoints !== undefined) {
1873
+ if (!isObject(raw.endpoints)) {
1874
+ err("endpoints", "must be object", raw.endpoints);
1875
+ } else {
1876
+ const epRaw = raw.endpoints as Record<string, unknown>;
1877
+ const ep: EndpointOverrides = {};
1878
+ const allowedEp = [
1879
+ "prod",
1880
+ "daily",
1881
+ "sandbox",
1882
+ "autopush",
1883
+ "quota",
1884
+ "models",
1885
+ "gemini_cli",
1886
+ "cloud_companion",
1887
+ "generate",
1888
+ "stream_generate",
1889
+ ];
1890
+ for (const k of Object.keys(epRaw)) {
1891
+ if (!allowedEp.includes(k)) {
1892
+ err(`endpoints.${k}`, "unknown endpoint key", epRaw[k]);
1893
+ continue;
1894
+ }
1895
+ const v = epRaw[k];
1896
+ if (v === undefined) continue;
1897
+ if (typeof v !== "string" || !isValidUri(v)) {
1898
+ err(`endpoints.${k}`, "must be valid https uri", v);
1899
+ } else {
1900
+ (ep as any)[k] = v;
1901
+ }
1902
+ }
1903
+ out.endpoints = deepMerge(DEFAULT_ENDPOINTS as any, ep as any);
1904
+ }
1905
+ }
1906
+
1907
+ // quota sub-config
1908
+ if (raw.quota !== undefined) {
1909
+ if (!isObject(raw.quota)) {
1910
+ err("quota", "must be object", raw.quota);
1911
+ } else {
1912
+ const qRaw = raw.quota as Record<string, unknown>;
1913
+ const q: QuotaSettings = {};
1914
+ if (qRaw.refresh_interval_minutes !== undefined) {
1915
+ if (
1916
+ !Number.isInteger(qRaw.refresh_interval_minutes) ||
1917
+ (qRaw.refresh_interval_minutes as number) < 1 ||
1918
+ (qRaw.refresh_interval_minutes as number) > 120
1919
+ )
1920
+ err("quota.refresh_interval_minutes", "must be 1-120", qRaw.refresh_interval_minutes);
1921
+ else q.refresh_interval_minutes = qRaw.refresh_interval_minutes as number;
1922
+ }
1923
+ if (qRaw.cache_ttl_minutes !== undefined) {
1924
+ if (
1925
+ !Number.isInteger(qRaw.cache_ttl_minutes) ||
1926
+ (qRaw.cache_ttl_minutes as number) < 1 ||
1927
+ (qRaw.cache_ttl_minutes as number) > 60
1928
+ )
1929
+ err("quota.cache_ttl_minutes", "must be 1-60", qRaw.cache_ttl_minutes);
1930
+ else q.cache_ttl_minutes = qRaw.cache_ttl_minutes as number;
1931
+ }
1932
+ if (qRaw.soft_threshold !== undefined) {
1933
+ if (
1934
+ typeof qRaw.soft_threshold !== "number" ||
1935
+ (qRaw.soft_threshold as number) < 0.1 ||
1936
+ (qRaw.soft_threshold as number) > 1
1937
+ )
1938
+ err("quota.soft_threshold", "must be 0.1-1.0", qRaw.soft_threshold);
1939
+ else q.soft_threshold = qRaw.soft_threshold as number;
1940
+ }
1941
+ if (qRaw.hard_threshold !== undefined) {
1942
+ if (
1943
+ typeof qRaw.hard_threshold !== "number" ||
1944
+ (qRaw.hard_threshold as number) < 0.5 ||
1945
+ (qRaw.hard_threshold as number) > 1
1946
+ )
1947
+ err("quota.hard_threshold", "must be 0.5-1.0", qRaw.hard_threshold);
1948
+ else q.hard_threshold = qRaw.hard_threshold as number;
1949
+ }
1950
+ if (qRaw.dual_pool !== undefined) {
1951
+ if (typeof qRaw.dual_pool !== "boolean") err("quota.dual_pool", "must be boolean", qRaw.dual_pool);
1952
+ else q.dual_pool = qRaw.dual_pool;
1953
+ }
1954
+ for (const k of Object.keys(qRaw)) {
1955
+ if (
1956
+ !["refresh_interval_minutes", "cache_ttl_minutes", "soft_threshold", "hard_threshold", "dual_pool"].includes(
1957
+ k,
1958
+ )
1959
+ )
1960
+ err(`quota.${k}`, "unknown property", qRaw[k]);
1961
+ }
1962
+ out.quota = deepMerge(DEFAULT_QUOTA as any, q as any);
1963
+ // sync top-level interval
1964
+ if (q.refresh_interval_minutes !== undefined) out.quota_refresh_interval_minutes = q.refresh_interval_minutes;
1965
+ }
1966
+ }
1967
+
1968
+ // keep top-level interval synced to quota object if defined oppositely
1969
+ if (out.quota_refresh_interval_minutes !== undefined) {
1970
+ if (!out.quota) out.quota = { ...DEFAULT_QUOTA };
1971
+ out.quota.refresh_interval_minutes = out.quota_refresh_interval_minutes;
1972
+ }
1973
+
1974
+ // bypass
1975
+ if (raw.bypass !== undefined) {
1976
+ if (!isObject(raw.bypass)) {
1977
+ err("bypass", "must be object", raw.bypass);
1978
+ } else {
1979
+ const bRaw = raw.bypass as Record<string, unknown>;
1980
+ const b: BypassMetadata = {};
1981
+ const semverRe = /^\d+\.\d+\.\d+/;
1982
+ if (bRaw.version_fallback !== undefined) {
1983
+ if (typeof bRaw.version_fallback !== "string" || !semverRe.test(bRaw.version_fallback))
1984
+ err("bypass.version_fallback", "must be semver x.y.z", bRaw.version_fallback);
1985
+ else b.version_fallback = bRaw.version_fallback;
1986
+ }
1987
+ if (bRaw.min_supported !== undefined) {
1988
+ if (typeof bRaw.min_supported !== "string" || !semverRe.test(bRaw.min_supported))
1989
+ err("bypass.min_supported", "must be semver", bRaw.min_supported);
1990
+ else b.min_supported = bRaw.min_supported;
1991
+ }
1992
+ if (bRaw.user_agent !== undefined) {
1993
+ if (typeof bRaw.user_agent !== "string" || !bRaw.user_agent.startsWith("antigravity/"))
1994
+ err("bypass.user_agent", "must start with antigravity/", bRaw.user_agent);
1995
+ else b.user_agent = bRaw.user_agent;
1996
+ }
1997
+ if (bRaw.strip_user_project_header !== undefined) {
1998
+ if (typeof bRaw.strip_user_project_header !== "boolean")
1999
+ err("bypass.strip_user_project_header", "must be boolean", bRaw.strip_user_project_header);
2000
+ else b.strip_user_project_header = bRaw.strip_user_project_header;
2001
+ }
2002
+ if (bRaw.fingerprint_jitter !== undefined) {
2003
+ if (typeof bRaw.fingerprint_jitter !== "boolean")
2004
+ err("bypass.fingerprint_jitter", "must be boolean", bRaw.fingerprint_jitter);
2005
+ else b.fingerprint_jitter = bRaw.fingerprint_jitter;
2006
+ }
2007
+ if (bRaw.pid_offset !== undefined) {
2008
+ if (typeof bRaw.pid_offset !== "boolean") err("bypass.pid_offset", "must be boolean", bRaw.pid_offset);
2009
+ else b.pid_offset = bRaw.pid_offset;
2010
+ }
2011
+ for (const k of Object.keys(bRaw)) {
2012
+ if (
2013
+ ![
2014
+ "version_fallback",
2015
+ "min_supported",
2016
+ "user_agent",
2017
+ "strip_user_project_header",
2018
+ "fingerprint_jitter",
2019
+ "pid_offset",
2020
+ ].includes(k)
2021
+ )
2022
+ err(`bypass.${k}`, "unknown property", bRaw[k]);
2023
+ }
2024
+ out.bypass = deepMerge(DEFAULT_BYPASS as any, b as any);
2025
+ // sync pid_offset_enabled
2026
+ if (b.pid_offset !== undefined) out.pid_offset_enabled = b.pid_offset;
2027
+ }
2028
+ }
2029
+ if (out.pid_offset_enabled !== undefined) {
2030
+ if (!out.bypass) out.bypass = { ...DEFAULT_BYPASS };
2031
+ out.bypass.pid_offset = out.pid_offset_enabled;
2032
+ }
2033
+
2034
+ // fallback_project_id
2035
+ if (raw.fallback_project_id !== undefined) {
2036
+ if (typeof raw.fallback_project_id !== "string" || (raw.fallback_project_id as string).length < 3)
2037
+ err("fallback_project_id", "must be string length >=3", raw.fallback_project_id);
2038
+ else out.fallback_project_id = raw.fallback_project_id;
2039
+ }
2040
+
2041
+ // default_model / search_model
2042
+ if (raw.default_model !== undefined) {
2043
+ if (typeof raw.default_model !== "string" || (raw.default_model as string).length < 3)
2044
+ err("default_model", "must be string", raw.default_model);
2045
+ else out.default_model = raw.default_model;
2046
+ }
2047
+ if (raw.search_model !== undefined) {
2048
+ if (typeof raw.search_model !== "string" || (raw.search_model as string).length < 3)
2049
+ err("search_model", "must be string", raw.search_model);
2050
+ else out.search_model = raw.search_model;
2051
+ }
2052
+
2053
+ // client_id / secret
2054
+ if (raw.client_id !== undefined) {
2055
+ if (raw.client_id !== null && typeof raw.client_id !== "string")
2056
+ err("client_id", "must be string|null", raw.client_id);
2057
+ else out.client_id = raw.client_id as string | null;
2058
+ }
2059
+ if (raw.client_secret !== undefined) {
2060
+ if (raw.client_secret !== null && typeof raw.client_secret !== "string")
2061
+ err("client_secret", "must be string|null", raw.client_secret);
2062
+ else out.client_secret = raw.client_secret as string | null;
2063
+ }
2064
+
2065
+ if (raw.accounts_file !== undefined) {
2066
+ if (raw.accounts_file !== null && typeof raw.accounts_file !== "string")
2067
+ err("accounts_file", "must be string|null", raw.accounts_file);
2068
+ else out.accounts_file = raw.accounts_file as string | null;
2069
+ }
2070
+
2071
+ if (raw.models_allowlist !== undefined) {
2072
+ if (
2073
+ !Array.isArray(raw.models_allowlist) ||
2074
+ !(raw.models_allowlist as unknown[]).every((v) => typeof v === "string")
2075
+ )
2076
+ err("models_allowlist", "must be string[]", raw.models_allowlist);
2077
+ else out.models_allowlist = raw.models_allowlist as string[];
2078
+ }
2079
+
2080
+ if (raw.strip_user_project !== undefined) {
2081
+ if (typeof raw.strip_user_project !== "boolean")
2082
+ err("strip_user_project", "must be boolean", raw.strip_user_project);
2083
+ else out.strip_user_project = raw.strip_user_project;
2084
+ }
2085
+
2086
+ // $schema field optional uri
2087
+ if (raw.$schema !== undefined) {
2088
+ if (typeof raw.$schema !== "string") err("$schema", "must be string uri", raw.$schema);
2089
+ else out.$schema = raw.$schema;
2090
+ }
2091
+
2092
+ // top-level unknown keys detection — strict production.
2093
+ // Note: v1-only fields are accepted so legacy configs pass strict validation.
2094
+ const allowedTop = [
2095
+ "$schema",
2096
+ "version",
2097
+ "cli_first",
2098
+ "pid_offset_enabled",
2099
+ "quota_refresh_interval_minutes",
2100
+ "rotation_strategy",
2101
+ "account_rotation",
2102
+ "debug",
2103
+ "debug_enabled",
2104
+ "google_search_enabled",
2105
+ "google_search",
2106
+ "endpoints",
2107
+ "quota",
2108
+ "bypass",
2109
+ "fallback_project_id",
2110
+ "default_model",
2111
+ "search_model",
2112
+ "client_id",
2113
+ "client_secret",
2114
+ "accounts_file",
2115
+ "models_allowlist",
2116
+ "strip_user_project",
2117
+ // v1-only fields accepted by the union interface
2118
+ "toast_scope",
2119
+ "soft_quota_threshold_percent",
2120
+ "quota_fallback",
2121
+ "soft_quota_cache_ttl_minutes",
2122
+ "version_cache_ttl",
2123
+ ];
2124
+ for (const k of Object.keys(raw)) {
2125
+ if (!allowedTop.includes(k)) err(k, "unknown property — strict schema", raw[k]);
2126
+ }
2127
+
2128
+ const valid = errors.length === 0;
2129
+ return { valid, errors, config: valid ? (out as AntigravityConfig) : undefined };
2130
+ }
2131
+
2132
+ // ---------------------------------------------------------------------------
2133
+ // Validation — manual validators without zod (v1)
2134
+ // ---------------------------------------------------------------------------
2135
+
2136
+ /**
2137
+ * Validates a numeric field within range; returns value or error entry.
2138
+ */
2139
+ function validateNumberInRange(
2140
+ key: string,
2141
+ value: unknown,
2142
+ min: number,
2143
+ max: number,
2144
+ defaultValue: number,
2145
+ errors: string[],
2146
+ ): number {
2147
+ if (value === undefined) return defaultValue;
2148
+ if (!isNumber(value)) {
2149
+ errors.push(`${key} must be a number`);
2150
+ return defaultValue;
2151
+ }
2152
+ if (value < min || value > max) {
2153
+ errors.push(`${key} must be between ${min} and ${max} (got ${value})`);
2154
+ // clamp instead of hard-fail for robustness
2155
+ return Math.min(Math.max(value, min), max);
2156
+ }
2157
+ return value;
2158
+ }
2159
+
2160
+ /**
2161
+ * Validates and normalizes a partial config object, merging with defaults.
2162
+ * No zod — manual validation for zero deps.
2163
+ *
2164
+ * @param raw - raw object read from JSON
2165
+ * @param opts.strict - when true, throws ConfigValidationError on errors; otherwise uses defaults and accumulates warnings
2166
+ * @returns sanitized config
2167
+ */
2168
+ export function parseAndValidateConfig(raw: unknown, opts?: { strict?: boolean }): AntigravityConfig {
2169
+ const strict = opts?.strict ?? false;
2170
+ const errors: string[] = [];
2171
+
2172
+ if (!isObject(raw)) {
2173
+ if (raw === null || raw === undefined) {
2174
+ // empty file/null => use defaults
2175
+ return { ...DEFAULT_CONFIG };
2176
+ }
2177
+ errors.push("config root must be an object");
2178
+ if (strict) throw new ConfigValidationError(errors);
2179
+ return { ...DEFAULT_CONFIG };
2180
+ }
2181
+
2182
+ // Start from defaults as base
2183
+ const result: AntigravityConfig = { ...DEFAULT_CONFIG };
2184
+
2185
+ // cli_first
2186
+ if ("cli_first" in raw) {
2187
+ if (!isBoolean(raw.cli_first)) {
2188
+ errors.push("cli_first must be boolean");
2189
+ } else {
2190
+ result.cli_first = raw.cli_first;
2191
+ }
2192
+ }
2193
+
2194
+ // toast_scope
2195
+ if ("toast_scope" in raw) {
2196
+ if (!isValidToastScope(raw.toast_scope)) {
2197
+ errors.push(`toast_scope must be one of all|project|minimal|none (got ${String(raw.toast_scope)})`);
2198
+ } else {
2199
+ result.toast_scope = raw.toast_scope;
2200
+ }
2201
+ }
2202
+
2203
+ // soft_quota_threshold_percent (default 90)
2204
+ result.soft_quota_threshold_percent = validateNumberInRange(
2205
+ "soft_quota_threshold_percent",
2206
+ (raw as Record<string, unknown>).soft_quota_threshold_percent,
2207
+ 0,
2208
+ 100,
2209
+ DEFAULT_CONFIG.soft_quota_threshold_percent!,
2210
+ errors,
2211
+ );
2212
+
2213
+ // quota_fallback
2214
+ if ("quota_fallback" in raw) {
2215
+ if (!isValidQuotaFallback(raw.quota_fallback)) {
2216
+ errors.push(`quota_fallback must be boolean | "auto" | modelName (got ${String(raw.quota_fallback)})`);
2217
+ } else {
2218
+ result.quota_fallback = raw.quota_fallback as QuotaFallback;
2219
+ }
2220
+ }
2221
+
2222
+ // quota_refresh_interval_minutes (default 15)
2223
+ result.quota_refresh_interval_minutes = validateNumberInRange(
2224
+ "quota_refresh_interval_minutes",
2225
+ (raw as Record<string, unknown>).quota_refresh_interval_minutes,
2226
+ 1,
2227
+ 1440,
2228
+ DEFAULT_CONFIG.quota_refresh_interval_minutes!,
2229
+ errors,
2230
+ );
2231
+
2232
+ // soft_quota_cache_ttl_minutes
2233
+ result.soft_quota_cache_ttl_minutes = validateNumberInRange(
2234
+ "soft_quota_cache_ttl_minutes",
2235
+ (raw as Record<string, unknown>).soft_quota_cache_ttl_minutes,
2236
+ 1,
2237
+ 1440,
2238
+ DEFAULT_CONFIG.soft_quota_cache_ttl_minutes!,
2239
+ errors,
2240
+ );
2241
+
2242
+ // pid_offset_enabled
2243
+ if ("pid_offset_enabled" in raw) {
2244
+ if (!isBoolean(raw.pid_offset_enabled)) {
2245
+ errors.push("pid_offset_enabled must be boolean");
2246
+ } else {
2247
+ result.pid_offset_enabled = raw.pid_offset_enabled;
2248
+ }
2249
+ }
2250
+
2251
+ // google_search_enabled
2252
+ if ("google_search_enabled" in raw) {
2253
+ if (!isBoolean(raw.google_search_enabled)) {
2254
+ errors.push("google_search_enabled must be boolean");
2255
+ } else {
2256
+ result.google_search_enabled = raw.google_search_enabled;
2257
+ }
2258
+ }
2259
+
2260
+ // debug
2261
+ if ("debug" in raw) {
2262
+ if (!isBoolean(raw.debug)) {
2263
+ errors.push("debug must be boolean");
2264
+ } else {
2265
+ result.debug = raw.debug;
2266
+ }
2267
+ }
2268
+
2269
+ // version_cache_ttl
2270
+ result.version_cache_ttl = validateNumberInRange(
2271
+ "version_cache_ttl",
2272
+ (raw as Record<string, unknown>).version_cache_ttl,
2273
+ 1,
2274
+ 10080,
2275
+ DEFAULT_CONFIG.version_cache_ttl!,
2276
+ errors,
2277
+ );
2278
+
2279
+ if (strict && errors.length > 0) {
2280
+ throw new ConfigValidationError(errors);
2281
+ }
2282
+
2283
+ // In non-strict mode the consumer can inspect errors via validateConfigRaw.
2284
+
2285
+ return result;
2286
+ }
2287
+
2288
+ /**
2289
+ * Lightweight validation returning the error list without throwing;
2290
+ * useful for diagnostics.
2291
+ */
2292
+ export function validateConfigRaw(raw: unknown): {
2293
+ valid: boolean;
2294
+ errors: string[];
2295
+ config: AntigravityConfig;
2296
+ } {
2297
+ const errors: string[] = [];
2298
+ let config: AntigravityConfig;
2299
+ try {
2300
+ // non-strict to sanitize
2301
+ config = parseAndValidateConfig(raw, { strict: false });
2302
+ // re-validate manually to collect every error
2303
+ if (!isObject(raw) && raw != null) {
2304
+ errors.push("config root must be an object");
2305
+ } else if (isObject(raw)) {
2306
+ if ("cli_first" in raw && !isBoolean(raw.cli_first)) errors.push("cli_first must be boolean");
2307
+ if ("toast_scope" in raw && !isValidToastScope(raw.toast_scope)) errors.push("toast_scope invalid");
2308
+ if ("quota_fallback" in raw && !isValidQuotaFallback(raw.quota_fallback)) errors.push("quota_fallback invalid");
2309
+ if ("pid_offset_enabled" in raw && !isBoolean(raw.pid_offset_enabled))
2310
+ errors.push("pid_offset_enabled must be boolean");
2311
+ if ("google_search_enabled" in raw && !isBoolean(raw.google_search_enabled))
2312
+ errors.push("google_search_enabled must be boolean");
2313
+ if ("debug" in raw && !isBoolean(raw.debug)) errors.push("debug must be boolean");
2314
+
2315
+ const numFields: Array<[string, number, number]> = [
2316
+ ["soft_quota_threshold_percent", 0, 100],
2317
+ ["quota_refresh_interval_minutes", 1, 1440],
2318
+ ["soft_quota_cache_ttl_minutes", 1, 1440],
2319
+ ["version_cache_ttl", 1, 10080],
2320
+ ];
2321
+ for (const [k, min, max] of numFields) {
2322
+ const v = (raw as Record<string, unknown>)[k];
2323
+ if (v !== undefined) {
2324
+ if (!isNumber(v)) errors.push(`${k} must be a number`);
2325
+ else if (v < min || v > max) errors.push(`${k} must be between ${min} and ${max}`);
2326
+ }
2327
+ }
2328
+ }
2329
+ } catch (e) {
2330
+ if (e instanceof ConfigValidationError) {
2331
+ const flat = e.errors.map((er) => (typeof er === "string" ? er : `${er.path}: ${er.message}`));
2332
+ return { valid: false, errors: flat.length ? flat : [(e as Error).message], config: { ...DEFAULT_CONFIG } };
2333
+ }
2334
+ errors.push((e as Error).message);
2335
+ config = { ...DEFAULT_CONFIG };
2336
+ }
2337
+
2338
+ return { valid: errors.length === 0, errors, config: config! };
2339
+ }
2340
+
2341
+ // ---------------------------------------------------------------------------
2342
+ // Defaults export
2343
+ // ---------------------------------------------------------------------------
2344
+
2345
+ /**
2346
+ * The observer returns fresh defaults without leaking references.
2347
+ */
2348
+ export function defaults(): AntigravityConfig {
2349
+ return getDefaultConfig();
2350
+ }
2351
+
2352
+ // ---------------------------------------------------------------------------
2353
+ // In-memory cache (v1) — mtime-based synchronous cache
2354
+ // ---------------------------------------------------------------------------
2355
+
2356
+ type MemoryCacheEntry = {
2357
+ config: AntigravityConfig;
2358
+ mtimeMs: number;
2359
+ path: string;
2360
+ loadedAtMs: number;
2361
+ };
2362
+
2363
+ let _memoryCache: MemoryCacheEntry | null = null;
2364
+
2365
+ // ---------------------------------------------------------------------------
2366
+ // Public API: load / save / update + memory cache (v1 synchronous cached)
2367
+ // ---------------------------------------------------------------------------
2368
+
2369
+ /**
2370
+ * Clears the in-memory cache (useful for tests or when the file changed externally).
2371
+ */
2372
+ export function clearConfigCache(): void {
2373
+ _memoryCache = null;
2374
+ }
2375
+
2376
+ /**
2377
+ * Loads configuration from disk with an mtime-based in-memory cache.
2378
+ * - If the file does not exist, returns defaults.
2379
+ * - If JSON is invalid, returns defaults (+ logs a warning when debug).
2380
+ * - Refreshes the in-memory cache.
2381
+ *
2382
+ * This is the preserved v1 synchronous cached API; other modules import it
2383
+ * by this exact name.
2384
+ *
2385
+ * @param opts - force: when true ignores the in-memory cache
2386
+ * @returns sanitized AntigravityConfig
2387
+ */
2388
+ export function loadConfig(opts?: { force?: boolean }): AntigravityConfig {
2389
+ const configPath = getConfigPath();
2390
+ const force = opts?.force ?? false;
2391
+
2392
+ // check in-memory cache while mtime is still valid
2393
+ if (!force && _memoryCache && _memoryCache.path === configPath) {
2394
+ try {
2395
+ const stat = fs.statSync(configPath);
2396
+ if (stat.mtimeMs === _memoryCache.mtimeMs) {
2397
+ return { ..._memoryCache.config };
2398
+ }
2399
+ } catch {
2400
+ // file may have been deleted
2401
+ if (!fs.existsSync(configPath)) {
2402
+ // file vanished: clear cache and return defaults
2403
+ clearConfigCache();
2404
+ return { ...DEFAULT_CONFIG };
2405
+ }
2406
+ // stat errored but file exists: return cached value as fallback
2407
+ return { ..._memoryCache.config };
2408
+ }
2409
+ }
2410
+
2411
+ // read disk
2412
+ let rawContent: string | null = null;
2413
+ let mtimeMs = 0;
2414
+
2415
+ try {
2416
+ const stat = fs.statSync(configPath);
2417
+ mtimeMs = stat.mtimeMs;
2418
+ rawContent = fs.readFileSync(configPath, { encoding: "utf8" });
2419
+ } catch (err) {
2420
+ // file does not exist => defaults
2421
+ if ((err as NodeJS.ErrnoException).code === "ENOENT" || !fs.existsSync(configPath)) {
2422
+ const cfg = { ...DEFAULT_CONFIG };
2423
+ _memoryCache = {
2424
+ config: cfg,
2425
+ mtimeMs: 0,
2426
+ path: configPath,
2427
+ loadedAtMs: Date.now(),
2428
+ };
2429
+ return { ...cfg };
2430
+ }
2431
+ // other read errors => defaults and clear cache
2432
+ clearConfigCache();
2433
+ return { ...DEFAULT_CONFIG };
2434
+ }
2435
+
2436
+ let parsed: unknown;
2437
+ try {
2438
+ parsed = rawContent ? JSON.parse(rawContent) : null;
2439
+ } catch {
2440
+ // invalid JSON => defaults
2441
+ const cfg = { ...DEFAULT_CONFIG };
2442
+ _memoryCache = {
2443
+ config: cfg,
2444
+ mtimeMs,
2445
+ path: configPath,
2446
+ loadedAtMs: Date.now(),
2447
+ };
2448
+ return { ...cfg };
2449
+ }
2450
+
2451
+ const cfg = parseAndValidateConfig(parsed, { strict: false });
2452
+
2453
+ _memoryCache = {
2454
+ config: cfg,
2455
+ mtimeMs,
2456
+ path: configPath,
2457
+ loadedAtMs: Date.now(),
2458
+ };
2459
+
2460
+ return { ...cfg };
2461
+ }
2462
+
2463
+ /**
2464
+ * Saves the full configuration atomically with chmod 0o600.
2465
+ * Refreshes the in-memory cache.
2466
+ *
2467
+ * Preserved v1 synchronous cached API.
2468
+ *
2469
+ * @param config - full (validated) config object
2470
+ * @returns saved config (clone)
2471
+ */
2472
+ export function agsaveConfig(config: AntigravityConfig): AntigravityConfig {
2473
+ // validate before saving (non-strict to sanitize)
2474
+ const sanitized = parseAndValidateConfig(config, { strict: false });
2475
+
2476
+ const configPath = getConfigPath();
2477
+ const json = JSON.stringify(sanitized, null, 2) + "\n";
2478
+
2479
+ atomicWriteFile(configPath, json);
2480
+
2481
+ // update cache with new mtime
2482
+ let mtimeMs = Date.now();
2483
+ try {
2484
+ const stat = fs.statSync(configPath);
2485
+ mtimeMs = stat.mtimeMs;
2486
+ } catch {
2487
+ // ignore
2488
+ }
2489
+
2490
+ _memoryCache = {
2491
+ config: sanitized,
2492
+ mtimeMs,
2493
+ path: configPath,
2494
+ loadedAtMs: Date.now(),
2495
+ };
2496
+
2497
+ return { ...sanitized };
2498
+ }
2499
+
2500
+ /**
2501
+ * Partially updates the config: shallow merge + atomic save.
2502
+ * Safe enough for single-process use; multi-process safety relies on atomic rename.
2503
+ *
2504
+ * @param partial - fields to update
2505
+ * @returns full updated config
2506
+ *
2507
+ * @example
2508
+ * updateConfig({ debug: true, toast_scope: "minimal" })
2509
+ */
2510
+ export function updateConfig(partial: Partial<AntigravityConfig>): AntigravityConfig {
2511
+ if (!isObject(partial)) {
2512
+ throw new TypeError("updateConfig: partial must be an object");
2513
+ }
2514
+
2515
+ // current state (uses cache when valid)
2516
+ const current = loadConfig();
2517
+
2518
+ // shallow merge: only known keys are accepted
2519
+ const mergedRaw: Record<string, unknown> = { ...current };
2520
+
2521
+ for (const key of Object.keys(partial) as Array<keyof AntigravityConfig>) {
2522
+ if (key in DEFAULT_CONFIG) {
2523
+ (mergedRaw as Record<string, unknown>)[key] = (partial as Record<string, unknown>)[key];
2524
+ }
2525
+ }
2526
+
2527
+ // validate merged
2528
+ const validated = parseAndValidateConfig(mergedRaw, { strict: false });
2529
+
2530
+ return agsaveConfig(validated);
2531
+ }
2532
+
2533
+ // ---------------------------------------------------------------------------
2534
+ // Async variants — v2 async implementations under the async names
2535
+ // ---------------------------------------------------------------------------
2536
+
2537
+ /**
2538
+ * Async loader (v2 implementation).
2539
+ *
2540
+ * - Passing a string `customPath` performs the v2 flow: read the given path,
2541
+ * merge defaults, strictly validate, throw ConfigValidationError on
2542
+ * corruption, return defaults silently when the file is missing.
2543
+ * - Passing `{ force }` or nothing delegates to the v1 synchronous cached
2544
+ * loader wrapped in a Promise (preserves the original v1 wrapper contract).
2545
+ *
2546
+ * @param customPathOrOpts - explicit config path (v2) or cache options (v1)
2547
+ */
2548
+ export async function loadConfigAsync(customPathOrOpts?: string | { force?: boolean }): Promise<AntigravityConfig> {
2549
+ if (typeof customPathOrOpts === "string") {
2550
+ return loadConfigFromPath(customPathOrOpts);
2551
+ }
2552
+ return loadConfig(customPathOrOpts);
2553
+ }
2554
+
2555
+ /**
2556
+ * v2 async path-based loader.
2557
+ * If the file is missing, returns defaults silently.
2558
+ * If corrupted, throws ConfigValidationError with path context.
2559
+ *
2560
+ * @param customPath - explicit config path
2561
+ */
2562
+ async function loadConfigFromPath(customPath?: string): Promise<AntigravityConfig> {
2563
+ const fp = resolveConfigPath(customPath);
2564
+ try {
2565
+ await fsp.access(fp, fs.constants.F_OK);
2566
+ } catch {
2567
+ // file does not exist — return defaults
2568
+ return getDefaultConfig();
2569
+ }
2570
+
2571
+ let rawText: string;
2572
+ try {
2573
+ rawText = await fsp.readFile(fp, "utf8");
2574
+ } catch (e: any) {
2575
+ throw new Error(`cannot read config ${fp}: ${e?.message ?? String(e)}`);
2576
+ }
2577
+
2578
+ if (!rawText.trim()) {
2579
+ return getDefaultConfig();
2580
+ }
2581
+
2582
+ let parsed: unknown;
2583
+ try {
2584
+ parsed = JSON.parse(rawText);
2585
+ } catch (e: any) {
2586
+ throw new ConfigValidationError(`invalid JSON in ${fp}: ${e?.message ?? String(e)}`, [
2587
+ { path: "$", message: `json parse error: ${e?.message}` },
2588
+ ]);
2589
+ }
2590
+
2591
+ // merge defaults <- parsed before validation to allow partial files
2592
+ const merged = deepMerge(getDefaultConfig() as any, parsed as any);
2593
+ const checked = validateInternal(merged);
2594
+ if (!checked.valid) {
2595
+ throw new ConfigValidationError(
2596
+ `config validation failed for ${fp}: ${checked.errors.map((er) => `${er.path} ${er.message}`).join(", ")}`,
2597
+ checked.errors,
2598
+ );
2599
+ }
2600
+ return checked.config!;
2601
+ }
2602
+
2603
+ /**
2604
+ * Async saver (v2 implementation): merges with defaults, validates, then
2605
+ * writes atomically (tmp + chmod 600 + rename).
2606
+ *
2607
+ * Note: returns the written file path (v2 semantics). Callers that need the
2608
+ * saved config clone can use the synchronous agsaveConfig.
2609
+ *
2610
+ * @param cfg - partial or full config, merged with defaults then validated
2611
+ * @param customPath - optional explicit path override
2612
+ * @returns written file path
2613
+ */
2614
+ export async function saveConfigAsync(
2615
+ cfg: Partial<AntigravityConfig> | AntigravityConfig,
2616
+ customPath?: string,
2617
+ ): Promise<string> {
2618
+ const fp = resolveConfigPath(customPath);
2619
+ // merge defaults + incoming, then validate
2620
+ const base = getDefaultConfig();
2621
+ const toSave = deepMerge(base as any, (cfg ?? {}) as any) as AntigravityConfig;
2622
+
2623
+ const checked = validateInternal(toSave);
2624
+ if (!checked.valid) {
2625
+ throw new ConfigValidationError(
2626
+ `cannot save invalid config: ${checked.errors.map((er) => `${er.path} ${er.message}`).join(", ")}`,
2627
+ checked.errors,
2628
+ );
2629
+ }
2630
+
2631
+ const finalCfg = checked.config!;
2632
+
2633
+ // JSON.stringify drops undefined values
2634
+ const content = JSON.stringify(finalCfg, null, 2) + "\n";
2635
+
2636
+ await atomicWriteFileAtomic(fp, content);
2637
+
2638
+ // for observability, log redacted version if debug enabled
2639
+ try {
2640
+ if (
2641
+ finalCfg.debug_enabled ||
2642
+ (typeof finalCfg.debug === "object" && (finalCfg.debug as DebugObjectConfig).enabled)
2643
+ ) {
2644
+ const redacted = redactedCopy(finalCfg);
2645
+ // observer does not throw on console failure
2646
+ if (typeof console !== "undefined" && console.debug) {
2647
+ console.debug(`[m[devthink] config saved ${fp}`, redacted);
2648
+ }
2649
+ }
2650
+ } catch {
2651
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2652
+ }
2653
+
2654
+ return fp;
2655
+ }
2656
+
2657
+ /**
2658
+ * Async version of updateConfig (v1 wrapper over the sync implementation).
2659
+ */
2660
+ export async function updateConfigAsync(partial: Partial<AntigravityConfig>): Promise<AntigravityConfig> {
2661
+ return updateConfig(partial);
2662
+ }
2663
+
2664
+ // ---------------------------------------------------------------------------
2665
+ // Sync bootstrap variants (v2) — direct sync IO without the mtime cache
2666
+ // ---------------------------------------------------------------------------
2667
+
2668
+ /**
2669
+ * Sync load — for CLI bootstrap where async is not convenient.
2670
+ * Same semantics as the v2 async loader but synchronous and uncached.
2671
+ */
2672
+ export function loadConfigSync(customPath?: string): AntigravityConfig {
2673
+ const fp = resolveConfigPath(customPath);
2674
+ try {
2675
+ fs.accessSync(fp, fs.constants.F_OK);
2676
+ } catch {
2677
+ return getDefaultConfig();
2678
+ }
2679
+ try {
2680
+ const txt = fs.readFileSync(fp, "utf8");
2681
+ if (!txt.trim()) return getDefaultConfig();
2682
+ const parsed = JSON.parse(txt);
2683
+ const merged = deepMerge(getDefaultConfig() as any, parsed as any);
2684
+ const checked = validateInternal(merged);
2685
+ if (!checked.valid) {
2686
+ throw new ConfigValidationError(`config validation failed for ${fp}`, checked.errors);
2687
+ }
2688
+ return checked.config!;
2689
+ } catch (e: any) {
2690
+ if (e instanceof ConfigValidationError) throw e;
2691
+ throw new Error(`cannot read config ${fp}: ${e?.message ?? String(e)}`);
2692
+ }
2693
+ }
2694
+
2695
+ /**
2696
+ * Sync save atomic — writes tmp, chmod, rename.
2697
+ */
2698
+ export function saveConfigSync(cfg: Partial<AntigravityConfig> | AntigravityConfig, customPath?: string): string {
2699
+ const fp = resolveConfigPath(customPath);
2700
+ const base = getDefaultConfig();
2701
+ const toSave = deepMerge(base as any, (cfg ?? {}) as any) as AntigravityConfig;
2702
+ const checked = validateInternal(toSave);
2703
+ if (!checked.valid) {
2704
+ throw new ConfigValidationError(
2705
+ `cannot save invalid config: ${checked.errors.map((er) => er.path).join(",")}`,
2706
+ checked.errors,
2707
+ );
2708
+ }
2709
+ const finalCfg = checked.config!;
2710
+ const content = JSON.stringify(finalCfg, null, 2) + "\n";
2711
+ const dir = path.dirname(fp);
2712
+ try {
2713
+ fs.mkdirSync(dir, { recursive: true, mode: DIR_MODE });
2714
+ } catch {
2715
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2716
+ }
2717
+ try {
2718
+ fs.chmodSync(dir, DIR_MODE);
2719
+ } catch {
2720
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2721
+ }
2722
+ const random = crypto.randomBytes(6).toString("hex");
2723
+ const tmp = path.join(dir, `.${path.basename(fp)}.${process.pid}.${random}.tmp`);
2724
+ try {
2725
+ fs.writeFileSync(tmp, content, { encoding: "utf8", mode: FILE_MODE });
2726
+ try {
2727
+ fs.chmodSync(tmp, FILE_MODE);
2728
+ } catch {
2729
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2730
+ }
2731
+ fs.renameSync(tmp, fp);
2732
+ try {
2733
+ fs.chmodSync(fp, FILE_MODE);
2734
+ } catch {
2735
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2736
+ }
2737
+ } finally {
2738
+ try {
2739
+ fs.unlinkSync(tmp);
2740
+ } catch {
2741
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2742
+ }
2743
+ }
2744
+ return fp;
2745
+ }
2746
+
2747
+ // ---------------------------------------------------------------------------
2748
+ // Safe debug info bundle (v1) — never logs secrets
2749
+ // ---------------------------------------------------------------------------
2750
+
2751
+ /**
2752
+ * Safe debug info (no secret). Intended for logging with debug enabled.
2753
+ */
2754
+ export function getSafeDebugInfo() {
2755
+ const cfgPath = getConfigPath();
2756
+ const tuple = getPlatformTuple();
2757
+ return {
2758
+ configPath: cfgPath,
2759
+ clientId: OAUTH_CLIENT_ID,
2760
+ projectFallback: PROJECT_FALLBACK,
2761
+ endpoints: CLOUDCODE_ENDPOINTS,
2762
+ modelsCount: MODELS_2026.length,
2763
+ platform: tuple.platform,
2764
+ arch: tuple.arch,
2765
+ userAgent: buildGeminiUserAgent(),
2766
+ xGoog: buildXGoogApiClient(),
2767
+ clientMetadata: CLIENT_METADATA_STRING,
2768
+ } as const;
2769
+ }
2770
+
2771
+ // ---------------------------------------------------------------------------
2772
+ // Exports barrel
2773
+ // ---------------------------------------------------------------------------
2774
+
2775
+ export const Config = {
2776
+ resolveConfigDir,
2777
+ resolveConfigPath,
2778
+ getConfigPath,
2779
+ getDefaultConfig,
2780
+ defaults,
2781
+ DEFAULT_CONFIG,
2782
+ validate,
2783
+ validateConfig,
2784
+ validateConfigRaw,
2785
+ parseAndValidateConfig,
2786
+ loadConfig,
2787
+ agsaveConfig,
2788
+ updateConfig,
2789
+ clearConfigCache,
2790
+ loadConfigAsync,
2791
+ saveConfigAsync,
2792
+ updateConfigAsync,
2793
+ loadConfigSync,
2794
+ saveConfigSync,
2795
+ getSafeDebugInfo,
2796
+ schema: ANTIGRAVITY_CONFIG_JSON_SCHEMA,
2797
+ defaultsRaw: CONFIG_DEFAULTS,
2798
+ };
2799
+
2800
+ export default Config;
2801
+
2802
+ // Compatibility alias for older imports
2803
+ export const SCHEMA = ANTIGRAVITY_CONFIG_JSON_SCHEMA;
2804
+
2805
+ // ---------------------------------------------------------------------------
2806
+ // Validation helpers — merged from validate.ts (v2.1.14 consolidation)
2807
+ // Canonical hardened validators: ReDoS-safe linear-time implementations,
2808
+ // documented, node:* only. The former validate.ts was deleted; its entire
2809
+ // surface lives below unchanged.
2810
+ // ---------------------------------------------------------------------------
2811
+
2812
+ /**
2813
+ * Validates an email address.
2814
+ * @param email - candidate email (nullish / non-string inputs coerce to "")
2815
+ * @returns true when the address has exactly one "@" and a dotted domain
2816
+ */
2817
+ export function isValidEmail(email: string): boolean {
2818
+ // Linear-time validation (regex-free; the former /^[^\s@]+@[^\s@]+\.[^\s@]+$/
2819
+ // had nested quantifiers and was flagged as ReDoS-prone on uncontrolled input).
2820
+ const value = String(email ?? "").trim();
2821
+ if (value.length === 0 || /\s/.test(value)) return false;
2822
+ const at = value.indexOf("@");
2823
+ if (at <= 0 || value.indexOf("@", at + 1) !== -1) return false;
2824
+ const domain = value.slice(at + 1);
2825
+ const dot = domain.lastIndexOf(".");
2826
+ return dot > 0 && dot < domain.length - 1;
2827
+ }
2828
+
2829
+ /**
2830
+ * Validates a semantic-version string (optional prerelease / build suffix).
2831
+ * @param v - candidate version
2832
+ */
2833
+ export function isValidSemver(v: string): boolean {
2834
+ return /^\d+\.\d+\.\d+(-[a-z0-9.-]+)?(\+[a-z0-9.-]+)?$/i.test(String(v ?? "").trim());
2835
+ }
2836
+
2837
+ /**
2838
+ * Validates a Google-style project id (lowercase, 6..30 chars) or the
2839
+ * documented fallback project id.
2840
+ * @param id - candidate project id
2841
+ */
2842
+ export function isValidProjectId(id: string): boolean {
2843
+ return /^[a-z][a-z0-9-]{4,29}[a-z0-9]$/.test(String(id ?? "").trim()) || id === "rising-fact-p41fc";
2844
+ }
2845
+
2846
+ /**
2847
+ * Validates a persisted account shape (email + refresh token).
2848
+ * @param a - candidate account object
2849
+ * @returns valid flag plus a human-readable error list
2850
+ */
2851
+ export function validateAccount(a: any): { valid: boolean; errors: string[] } {
2852
+ const errors: string[] = [];
2853
+ if (!a) errors.push("account is null");
2854
+ else {
2855
+ if (!a.email || !isValidEmail(a.email)) errors.push("invalid email");
2856
+ if (!a.refreshToken || typeof a.refreshToken !== "string" || a.refreshToken.length < 10)
2857
+ errors.push("invalid refreshToken");
2858
+ }
2859
+ return { valid: errors.length === 0, errors };
2860
+ }
2861
+
2862
+ /**
2863
+ * Validates a model id semantically: the lowercased id must contain one of
2864
+ * the substrings gemini / claude / antigravity.
2865
+ * @param model - candidate model id
2866
+ */
2867
+ export function validateModelId(model: string): boolean {
2868
+ const lower = String(model ?? "").toLowerCase();
2869
+ return lower.includes("gemini") || lower.includes("claude") || lower.includes("antigravity");
2870
+ }
2871
+
2872
+ // ---------------------------------------------------------------------------
2873
+ // System utilities — merged from system.ts (v2.1.14 consolidation)
2874
+ // Survivors of the collision resolution, kept verbatim from the former
2875
+ // system.ts (dense style) except for the documented renames below; node:*
2876
+ // references are qualified through the fs / path / os / crypto namespaces
2877
+ // imported at the top of this file.
2878
+ // - validateEmail / validateSemver / validateProjectId: legacy system.ts
2879
+ // names, now one-line aliases of the canonical validators above (the old
2880
+ // regex implementations were the same-purpose, weaker duplicates).
2881
+ // - validateModelIdCharset: former system.ts validateModelId (charset
2882
+ // regex), renamed to free the canonical semantic validateModelId.
2883
+ // - loadConfigRaw / saveConfigRaw: former system.ts minimal raw JSON IO
2884
+ // helpers, renamed because the validated loadConfig / agsaveConfig earlier
2885
+ // in this file keep the canonical names.
2886
+ // - redactSecrets / VERSION_FALLBACK / fetchVersionChain stay config-local
2887
+ // this round; cross-file dedupe (debug.ts, version.ts, auth.ts) is a
2888
+ // follow-up orchestrator concern.
2889
+ // ---------------------------------------------------------------------------
2890
+
2891
+ /** Config directory (honors OPENCODE_CONFIG_DIR, defaults to ~/.config/opencode). */
2892
+ export const cfgDir = (): string =>
2893
+ process.env.OPENCODE_CONFIG_DIR?.trim() || path.join(os.homedir(), ".config", "opencode");
2894
+
2895
+ // v2.1.15 Phase B: the logging trio (logsDir, redactSecrets with the
2896
+ // Bearer [REDACTED] semantics, and the debugLogger ring-buffer singleton)
2897
+ // moved to debug.ts — THE logging owner. debug.ts imports cfgDir from here
2898
+ // (one-directional; config never imports debug).
2899
+ /** Governed fallback version when every version-chain source fails. */
2900
+ export const VERSION_FALLBACK = "1.19.2" as const;
2901
+
2902
+ /** Fetches the latest Antigravity version across the known update sources. */
2903
+ export const fetchVersionChain = async (): Promise<string> => {
2904
+ const urls = [
2905
+ "https://autoupdater.antigravity.google.com/version",
2906
+ "https://registry.npmjs.org/antigravity/latest",
2907
+ "https://api.github.com/repos/antigravity/antigravity/releases/latest",
2908
+ ];
2909
+ for (const u of urls) {
2910
+ try {
2911
+ const r = await fetch(u, { headers: { "User-Agent": "antigravity/1.19.2" } });
2912
+ if (!r.ok) continue;
2913
+ const j: any = await r.json().catch(async () => ({ text: await r.text() }));
2914
+ const v = j.version || j.tag_name || j.text || "";
2915
+ if (v) return String(v).replace(/^v/, "");
2916
+ } catch {
2917
+ continue;
2918
+ }
2919
+ }
2920
+ return VERSION_FALLBACK;
2921
+ };
2922
+
2923
+ // Legacy system.ts validator names — thin aliases of the canonical hardened
2924
+ // validators in the validation-helpers section (kept for the published
2925
+ // ./system subpath surface).
2926
+ export const validateEmail = isValidEmail;
2927
+ export const validateSemver = isValidSemver;
2928
+ export const validateProjectId = isValidProjectId;
2929
+
2930
+ /** Charset regex variant of model-id validation (former system.ts validateModelId). */
2931
+ export const validateModelIdCharset = (id: string): boolean => /^[a-z0-9-_.]+$/.test(id.toLowerCase());
2932
+
2933
+ /** Minimal raw JSON read of antigravity.json (no schema validation, never throws). */
2934
+ export const loadConfigRaw = (): any => {
2935
+ try {
2936
+ const p = path.join(cfgDir(), "antigravity.json");
2937
+ if (!fs.existsSync(p)) return {};
2938
+ const raw = fs.readFileSync(p, "utf8");
2939
+ return JSON.parse(raw);
2940
+ } catch {
2941
+ return {};
2942
+ }
2943
+ };
2944
+
2945
+ /** Minimal raw JSON write of antigravity.json (atomic tmp+rename, chmod 600, never throws). */
2946
+ export const saveConfigRaw = (c: any): void => {
2947
+ try {
2948
+ fs.mkdirSync(cfgDir(), { recursive: true, mode: 0o700 });
2949
+ const p = path.join(cfgDir(), "antigravity.json");
2950
+ const tmp = `${p}.tmp-${process.pid}-${crypto.randomInt(1000000)}`;
2951
+ fs.writeFileSync(tmp, JSON.stringify(c, null, 2), { encoding: "utf8", mode: 0o600 });
2952
+ try {
2953
+ fs.chmodSync(tmp, 0o600);
2954
+ } catch {
2955
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2956
+ }
2957
+ try {
2958
+ fs.renameSync(tmp, p);
2959
+ } catch {
2960
+ fs.writeFileSync(p, JSON.stringify(c, null, 2), { encoding: "utf8", mode: 0o600 });
2961
+ }
2962
+ try {
2963
+ fs.chmodSync(p, 0o600);
2964
+ } catch {
2965
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2966
+ }
2967
+ } catch {
2968
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2969
+ }
2970
+ };
2971
+
2972
+ /** Atomic file write (tmp + chmod 600 + rename) used for plain-file persistence. */
2973
+ export const atomicWrite = (p: string, c: string): void => {
2974
+ try {
2975
+ fs.mkdirSync(cfgDir(), { recursive: true });
2976
+ } catch {
2977
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2978
+ }
2979
+ const tmp = `${p}.tmp-${crypto.randomInt(1000000)}`;
2980
+ fs.writeFileSync(tmp, c, { encoding: "utf8", mode: 0o600 });
2981
+ try {
2982
+ fs.renameSync(tmp, p);
2983
+ } catch {
2984
+ fs.writeFileSync(p, c, { encoding: "utf8" });
2985
+ }
2986
+ };
2987
+
2988
+ /* ════════════════════════════════════════════════════════════════════
2989
+ Section: the gateway configuration loader (the 1.1.13 embedded-gateway
2990
+ definition loader absorbed by the grand merge — the .mjs-first probe
2991
+ chain, the version registry and the validator).
2992
+ ════════════════════════════════════════════════════════════════════ */
2993
+ /**
2994
+ * configloader — loads the gateway definition from the user customization layer
2995
+ * one file one responsibility — only config loading lives here
2996
+ *
2997
+ * the library is universal and dry — all logic lives at root
2998
+ * the user customization lives in web/config.ts — the hardcoded v1 v5
2999
+ * definitions that ship as the default example
3000
+ *
3001
+ * resolution order:
3002
+ * 1 gateway.config.ts in cwd — user placed config at project root
3003
+ * 2 web/config.ts — the standard location per architecture skill
3004
+ * 3 builtin default — the shipped v1 v5 example configs
3005
+ *
3006
+ * the cli scaffolds a fresh web/config.ts for new users
3007
+ * users may create as many versions as they want — v1 through v9 and beyond
3008
+ */
3009
+
3010
+ import { dirname, resolve } from "node:path";
3011
+ import { fileURLToPath } from "node:url";
3012
+ import type { gatewayconfig, gatewaydefinition } from "./types";
3013
+
3014
+ /** loaded definition cache */
3015
+ let loaded: gatewaydefinition | null = null;
3016
+
3017
+ /** builtin default — empty definition when no user config exists
3018
+ * the library is dry — user customization lives in web/config.ts
3019
+ * the cli scaffolds a fresh web/config.ts for new users */
3020
+ async function builtindefault(): Promise<gatewaydefinition> {
3021
+ return { versions: {} };
3022
+ }
3023
+
3024
+ /** try import from a path — returns null when not found */
3025
+ async function tryimport(path: string): Promise<gatewaydefinition | null> {
3026
+ try {
3027
+ const mod = await import(/* @vite-ignore */ path);
3028
+ const candidate = mod.default ?? mod.config ?? mod.gatewaydefinition ?? mod;
3029
+ if (candidate && typeof candidate === "object" && "versions" in candidate) {
3030
+ return candidate as gatewaydefinition;
3031
+ }
3032
+ return null;
3033
+ } catch {
3034
+ return null;
3035
+ }
3036
+ }
3037
+
3038
+ /** config search bases — cwd, module dir (source root) and module parent (dist)
3039
+ * relative dynamic imports resolve against the importing module url not
3040
+ * the process cwd, so the built dist/configloader.js must also probe the
3041
+ * parent directory where web/config.ts lives in a packaged install */
3042
+ function searchbases(): string[] {
3043
+ const moduledir = dirname(fileURLToPath(import.meta.url));
3044
+ const bases = [resolve(process.cwd()), moduledir, resolve(moduledir, "..")];
3045
+ return [...new Set(bases)];
3046
+ }
3047
+
3048
+ /** loadconfig — resolve the gateway definition from the standard locations
3049
+ * caches the result for the process lifetime — call reloadconfig to refresh */
3050
+ export async function loadconfig(): Promise<gatewaydefinition> {
3051
+ if (loaded) return loaded;
3052
+ // .mjs variants come first in each pair: the extension carries the module
3053
+ // type (immune to the nearest package.json type field), so a config
3054
+ // scaffolded by the cli loads under plain node in every consumer project
3055
+ const names = [
3056
+ "web/config.mjs",
3057
+ "web/config.ts",
3058
+ "web/config.js",
3059
+ "gateway.config.mjs",
3060
+ "gateway.config.ts",
3061
+ "gateway.config.js",
3062
+ ];
3063
+ for (const base of searchbases()) {
3064
+ for (const name of names) {
3065
+ const found = await tryimport(`${base}/${name}`);
3066
+ if (found) {
3067
+ loaded = found;
3068
+ return found;
3069
+ }
3070
+ }
3071
+ }
3072
+ // builtin fallback
3073
+ loaded = await builtindefault();
3074
+ return loaded;
3075
+ }
3076
+
3077
+ /** reloadconfig — clear the cache and reload from disk */
3078
+ export async function reloadconfig(): Promise<gatewaydefinition> {
3079
+ loaded = null;
3080
+ return loadconfig();
3081
+ }
3082
+
3083
+ /** getversion — get one version config by id */
3084
+ export async function getversion(id: string): Promise<gatewayconfig | null> {
3085
+ const def = await loadconfig();
3086
+ return def.versions[id] ?? null;
3087
+ }
3088
+
3089
+ /** listversions — get all version ids */
3090
+ export async function listversions(): Promise<string[]> {
3091
+ const def = await loadconfig();
3092
+ return Object.keys(def.versions);
3093
+ }
3094
+
3095
+ /** validateconfig — check a gateway definition for common errors
3096
+ * returns a list of human readable problems — empty when valid */
3097
+ export function validateconfig(def: gatewaydefinition): string[] {
3098
+ const problems: string[] = [];
3099
+ if (!def.versions || Object.keys(def.versions).length === 0) {
3100
+ problems.push("no versions defined — add at least one version to versions map");
3101
+ return problems;
3102
+ }
3103
+ for (const [id, cfg] of Object.entries(def.versions)) {
3104
+ const prefix = `version ${id}:`;
3105
+ if (!cfg.id) problems.push(`${prefix} missing id`);
3106
+ if (cfg.id !== id) problems.push(`${prefix} id mismatch — map key is ${id} but id is ${cfg.id}`);
3107
+ if (!cfg.providername) problems.push(`${prefix} missing providername`);
3108
+ if (!cfg.upstreams || cfg.upstreams.length === 0) problems.push(`${prefix} no upstreams configured`);
3109
+ if (!cfg.auth) problems.push(`${prefix} missing auth config`);
3110
+ if (!cfg.models || cfg.models.length === 0) problems.push(`${prefix} no models configured`);
3111
+ if (!cfg.metamodel?.id) problems.push(`${prefix} missing metamodel id`);
3112
+ // rotation models must exist in the catalog — the catalog may itself be
3113
+ // missing (already reported above) so the lookup guards against it
3114
+ if (cfg.rotation?.models) {
3115
+ const ids = new Set((cfg.models ?? []).map((m) => m.id));
3116
+ for (const rm of cfg.rotation.models) {
3117
+ if (!ids.has(rm)) problems.push(`${prefix} rotation model ${rm} not in models catalog`);
3118
+ }
3119
+ }
3120
+ // defaultmodel must exist
3121
+ if (
3122
+ cfg.defaultmodel &&
3123
+ !(cfg.models ?? []).find((m) => m.id === cfg.defaultmodel) &&
3124
+ cfg.defaultmodel !== cfg.metamodel?.id
3125
+ ) {
3126
+ problems.push(`${prefix} defaultmodel ${cfg.defaultmodel} not in models catalog`);
3127
+ }
3128
+ // auth env var must be set when required from env — a version with a
3129
+ // missing auth block already carries its problem above; the dereference
3130
+ // below used to crash the whole validation with a type error instead of
3131
+ // reporting the remaining problems
3132
+ if (cfg.auth?.required && cfg.auth?.keysources?.includes("env") && !cfg.auth?.envvar) {
3133
+ problems.push(`${prefix} auth requires env keys but envvar is not set`);
3134
+ }
3135
+ }
3136
+ return problems;
3137
+ }