@wenathlan/devthink 2.0.15 → 2.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/antigravity.ts DELETED
@@ -1,2139 +0,0 @@
1
- /* ── Merged: the grand merge section ── the correlated antigravity provider plugin logics of the merged repository interned here, one surface without duplicate variations ── */
2
- /**
3
- * @file antigravity.ts — the antigravity provider plugin of the merged repository.
4
- * @description
5
- * Third-person observer: the observer analyses how Gemini CLI, OmniRoute, 9router and
6
- * pi-antigravity-auth bypass the Project ID and reproduces exactly the same behaviour
7
- * in a library-first, root-first shape.
8
- *
9
- * Responsabilidades (the merged plugin contract):
10
- * - Total Project ID bypass acting exactly like the Gemini CLI: when
11
- * account.projectId is absent the observer calls resolveProjectId which runs
12
- * loadCodeAssist without a project (metadata only), onboardUser when needed
13
- * (FREE tier), LRO polling, never demanding a project from the user. The
14
- * in-memory cache carries a truncated SHA256 hash so a heap snapshot never
15
- * leaks the token.
16
- * - Endpoint wrapping {model, project, request} the Gemini CLI way
17
- * (CodeAssistServer.createCodeAssistContentGenerator), straight to
18
- * cloudcode-pa.googleapis.com, no localhost v1 proxy, no generativelanguage.
19
- * - The complete model list through 25/08/2026: gemini-3.6, 3.5, 3.1, 3,
20
- * 2.5, claude 4.5/4.6 thinking, gpt-oss-120b, preview variants plus the
21
- * antigravity-* aliases.
22
- * - Dual quota (Antigravity + Gemini CLI), cli_first, pid_offset_enabled
23
- * 0-1000, google_search grounding.
24
- * - Hooks: the fetch interceptor cascade 403/404/5xx with 0-80ms jitter,
25
- * strip x-goog-user-project (fix #1830), skip sandbox for
26
- * gemini-3.6/3.5/preview (fix #233), the third-person observer, node:*
27
- * builtins + global fetch only.
28
- */
29
-
30
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
31
- import * as fsp from "node:fs/promises";
32
- import { join, resolve, dirname } from "node:path";
33
- import { homedir, platform as osPlatform, arch as osArch, release as osRelease } from "node:os";
34
- import { randomInt, createHash, randomBytes } from "node:crypto";
35
- // merged siblings (base names only)
36
- import {
37
- getGlobalSignatureCache,
38
- getGlobalPreservationStore,
39
- isValidThinkingSignature,
40
- stripInvalidSignatures,
41
- } from "./recovery.js";
42
- import * as oauthMod from "./oauth.js";
43
- import * as accountsMod from "./accounts.js";
44
- import {
45
- resolveOAuthIdentity,
46
- identityFromAccountType,
47
- getAntigravityCliHeaders,
48
- ANTIGRAVITY_CLI_OAUTH_SCOPES,
49
- } from "./oauth.js";
50
- import { getVersion } from "./versionregistry.js";
51
- // v2.1.14 dedupe: stateless constants imported from their single owners
52
- // (constants.ts / models.ts) instead of local copies. Runtime functions and
53
- // stateful caches stay local to this Layer-1 entry by design.
54
- // v2.1.15 Phase A: every model-identification family (catalog lists, routing
55
- // classifiers, thinking tables, endpoint orders, image-gen helpers) is
56
- // imported from models.ts — THE single owner — and project discovery comes
57
- // from project.ts — THE single project owner; the former local copies were
58
- // deleted and re-exported under their historical names.
59
- import {
60
- ANTIGRAVITY_VERSION_FALLBACK,
61
- CLIENT_METADATA_STRING,
62
- CLOUDCODE_BASE_URL,
63
- CONTENT_TIMEOUT_MS,
64
- FALLBACK_PROJECT_ID,
65
- GEMINI_CLI_USER_AGENT_PLAIN,
66
- PLUGIN_VERSION,
67
- } from "./constants.js";
68
- // v2.1.16 cleanup: FORBIDDEN_HEADERS / FETCH_TIMEOUT_MS / GEMINI_CLI_SCOPES
69
- // were imported but never used in the file body — they stay reachable through
70
- // the re-export block below, so the dead import bindings are gone.
71
- export type PluginOptions = {
72
- cli_first?: boolean;
73
- pid_offset_enabled?: boolean;
74
- /** true/false forces grounding on/off; "auto" enables it for Gemini (non-Claude) models only. */
75
- google_search_enabled?: boolean | "auto";
76
- quota_refresh_interval_minutes?: number;
77
- rotation_strategy?: "round-robin" | "sticky";
78
- debug?: boolean;
79
- /** Prefix tool descriptions with hardening hints for Claude models. */
80
- claude_tool_hardening?: boolean;
81
- /** Keep thinking blocks/signatures in transformed output instead of dropping them. */
82
- keep_thinking?: boolean;
83
- /** Skip accounts whose used-percentage is at or above this threshold (0-100). */
84
- soft_quota_threshold_percent?: number;
85
- /** How long a soft-quota mark lasts, in minutes; "auto" resolves to 15. */
86
- soft_quota_cache_ttl_minutes?: number | "auto";
87
- /** Suppress toast() notifications. */
88
- quiet_mode?: boolean;
89
- };
90
-
91
- // v2.1.15 Phase C: the Account interface moved to accounts.js (account store
92
- // owner) — imported type-only (erased at runtime).
93
-
94
- // v2.1.15 Phase B: generic utilities (fnv hash family, jitter) come from
95
- // core.js — THE owner; identity masquerade (dynamic UA, antigravity headers)
96
- // comes from fingerprint.js — THE owner.
97
- import { fnv1a32, fnv1a32Hex, getJitter } from "./core.js";
98
- import { buildAntagravityHeaders, getDynamicUserAgent } from "./fingerprint.js";
99
- import { X_GOOG_API_CLIENT_GEMINI_CLI } from "./fingerprint.js";
100
- import {
101
- ALL_MODELS_2026,
102
- ANTIGRAVITY_VERSION_MIN,
103
- CODE_ASSIST_ENDPOINTS,
104
- CONTEXT_WINDOW_CLAUDE,
105
- CONTEXT_WINDOW_GEMINI,
106
- CONTEXT_WINDOW_GPT_OSS,
107
- ENDPOINT_ORDER_GEMINI_CLI,
108
- MODELS_2026_PROVIDER,
109
- OUTPUT_LIMIT_CLAUDE,
110
- OUTPUT_LIMIT_GEMINI,
111
- OUTPUT_LIMIT_GPT_OSS,
112
- THINKING_BUDGET_MAP,
113
- THINKING_LEVELS,
114
- buildImageGenConfig,
115
- getEndpointsForModel,
116
- isGeminiCLIOnlyModel,
117
- isImageGenModel,
118
- mapModelToGroup,
119
- } from "./models.js";
120
- import type { ThinkingLevel } from "./models.js";
121
- import { resolveProjectId } from "./project.js";
122
- // v2.1.15 Phase D: the live request pipeline (transformRequest, the Cloud Code
123
- // builders, fetchWithCascade and the method paths) lives ONLY in request.ts;
124
- // the fetch-cascade recovery helpers live ONLY in recovery.ts.
125
- import {
126
- buildAntigravityRequest,
127
- buildGeminiCLIRequest,
128
- fetchWithCascade,
129
- transformRequest,
130
- STREAM_METHOD,
131
- GENERATE_METHOD,
132
- LOAD_METHOD,
133
- ONBOARD_METHOD,
134
- } from "./request.js";
135
- import type { BuildOptions, BuildResult } from "./request.js";
136
- import { detectFetchErrorType, fetchSessionRecovery, preserveValidatedSignature } from "./recovery.js";
137
- import { shouldSkipSandbox } from "./models.js";
138
- import { QUOTA_METHOD } from "./quota.js";
139
-
140
- import type { Account } from "./accounts.js";
141
- export type { Account };
142
- import type { ResolveProjectIdOptions, ResolveResult } from "./project.js";
143
-
144
- // v2.1.14 dedupe: re-export the consolidated constants so this module's
145
- // public surface is unchanged.
146
- export {
147
- ANTIGRAVITY_VERSION_FALLBACK,
148
- CONTENT_TIMEOUT_MS,
149
- FALLBACK_PROJECT_ID,
150
- FORBIDDEN_HEADERS,
151
- FETCH_TIMEOUT_MS,
152
- GEMINI_CLI_SCOPES,
153
- PLUGIN_VERSION,
154
- } from "./constants.js";
155
- export { fnv1a32, fnv1a32Hex, getJitter } from "./core.js";
156
- export { buildAntagravityHeaders, getDynamicUserAgent } from "./fingerprint.js";
157
- export {
158
- ALL_MODELS_2026,
159
- ANTIGRAVITY_VERSION_MIN,
160
- CODE_ASSIST_ENDPOINTS,
161
- ENDPOINT_ORDER,
162
- ENDPOINT_ORDER_GEMINI_CLI,
163
- GEMINI_CLI_ONLY_MODELS,
164
- MODELS_2026_BASE,
165
- MODELS_2026_ANTIGRAVITY_ALIAS,
166
- THINKING_BUDGET_MAP,
167
- THINKING_LEVELS,
168
- buildImageGenConfig,
169
- getEndpointsForModel,
170
- isGeminiCLIOnlyModel,
171
- isImageGenModel,
172
- mapModelToGroup,
173
- } from "./models.js";
174
- export { MODELS_2026_PROVIDER as MODELS_2026 } from "./models.js";
175
- export type { ModelId2026, ThinkingLevel } from "./models.js";
176
- export { resolveProjectId } from "./project.js";
177
- export type { ResolveProjectIdOptions, ResolveResult } from "./project.js";
178
-
179
- // Node types (no DOM lib) do not expose the RequestInfo name globally; derive it from fetch itself.
180
- type RequestInfo = Parameters<typeof fetch>[0];
181
-
182
- /**
183
- * Linear-time trailing-slash strip (regex-free; safe on uncontrolled input —
184
- * the former /\/+$/.replace was polynomial and flagged as ReDoS-prone).
185
- */
186
- function stripTrailingSlashes(value: string): string {
187
- let end = value.length;
188
- while (end > 0 && value.charCodeAt(end - 1) === 47 /* "/" */) end--;
189
- return end === value.length ? value : value.slice(0, end);
190
- }
191
-
192
- // --- optional internal modules (they themselves only use node:* + fetch) ---
193
- // The observer tries to reuse them when present, but v2 is self-contained to guarantee bypass logic.
194
- let __AccountManagerCtor: any = null;
195
- try {
196
- // dynamic import via top-level await not allowed here, will lazy import in init
197
- } catch {
198
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
199
- }
200
-
201
- // ---------------------------------------------------------------------------
202
- // Constants — frozen, deterministic
203
- // ---------------------------------------------------------------------------
204
-
205
- export const PLUGIN_ID = "devthink" as const;
206
- // PLUGIN_VERSION is imported from constants.ts (single owner) and re-exported above.
207
-
208
- /** Public alias kept for consumers that expect `VERSION` instead of `PLUGIN_VERSION`. */
209
- export const VERSION: string = PLUGIN_VERSION;
210
-
211
- /**
212
- * Gemini CLI identification fallback headers used when the auth module does
213
- * not expose getGeminiHeaders(). v2.1.16 single-owner fix: hoisted from the
214
- * two byte-identical inline copies (opencode auth loader + chat.headers
215
- * spoof) — every value comes from its owner: the plain UA (no model segment)
216
- * and the Client-Metadata string from constants.js, the gl-node snapshot
217
- * from fingerprint.js. The "gccl/0.9.2" composite segment is not exported
218
- * by any owner module, so the composite X-Goog-Api-Client is assembled
219
- * exactly once here (plugin.ts's single local definition, shared by both
220
- * sites — fixes the internal duplication found by audit 16-B).
221
- */
222
- const FALLBACK_GEMINI_HEADERS: Record<string, string> = {
223
- "User-Agent": GEMINI_CLI_USER_AGENT_PLAIN,
224
- "X-Goog-Api-Client": `${X_GOOG_API_CLIENT_GEMINI_CLI} gccl/0.9.2 ${X_GOOG_API_CLIENT_GEMINI_CLI}`,
225
- "Client-Metadata": CLIENT_METADATA_STRING,
226
- };
227
-
228
- // ---------------------------------------------------------------------------
229
- // Gemini CLI bypass credentials — exact identification used by the official CLI
230
- // (merged from legacy plugin.ts / plugin(1).ts; v2.1.14: values deduped to the
231
- // single owner constants.ts GEMINI_CLI_OAUTH_CLIENT_ID/SECRET)
232
- // ---------------------------------------------------------------------------
233
-
234
- // v2.1.15 Phase C: GEMINI_CLI_OAUTH_CLIENT_ID/SECRET were aliases of the
235
- // constants.js GEMINI_CLI_OAUTH_CLIENT_ID/SECRET pair — the aliases are
236
- // replaced by direct imports (single owner, single name).
237
-
238
- /** Short aliases for the Gemini CLI bypass credentials. */
239
- // v2.1.15 Phase C: the CLIENT_ID/CLIENT_SECRET aliases (gemini-cli pair)
240
- // were deleted — auth.js CLIENT_ID/CLIENT_SECRET is the antigravity-cli
241
- // client pair; the gemini-cli pair is GEMINI_CLI_OAUTH_CLIENT_ID/SECRET.
242
-
243
- // v2.1.15 Phase A: ALL_MODELS_2026 (flat routing catalog), the ALL_MODELS
244
- // alias, isImageGenModel and buildImageGenConfig moved to models.ts —
245
- // imported and re-exported at the top of this file.
246
-
247
- // v2.1.15 Phase D: the Cloud Code method paths (STREAM_METHOD, GENERATE_METHOD,
248
- // LOAD_METHOD, ONBOARD_METHOD) moved to request.ts; QUOTA_METHOD moved to
249
- // quota.ts — imported from the owners at the top of this file.
250
-
251
- // ---------------------------------------------------------------------------
252
- // Helpers — FNV, jitter, UA, headers, cache
253
- // ---------------------------------------------------------------------------
254
-
255
- // ---------------------------------------------------------------------------
256
- // Legacy header builders (merged from plugin.ts / plugin(1).ts) — kept for
257
- // consumers that import them directly; bypass flows above remain canonical.
258
- // ---------------------------------------------------------------------------
259
-
260
- /** Gemini CLI identification headers — THE bypass secret for CLI-pool models. */
261
-
262
- /** Legacy Antigravity desktop identification headers variant. */
263
-
264
- // ---------------------------------------------------------------------------
265
- // Notifications + error classification + retry-once recovery + thinking
266
- // signature preservation (merged from plugin(3)/plugin(4)/plugin(5) flows,
267
- // backed by the recovery module caches).
268
- // ---------------------------------------------------------------------------
269
-
270
- /** Module-level quiet flag toggled from PluginOptions.quiet_mode. */
271
- let __quietMode = false;
272
-
273
- /**
274
- * User-facing notification glue (v2.1.15 Phase D: renamed from the former
275
- * toast() — recovery.js owns the toast name with its sink-based notifier).
276
- * No-op when quiet_mode is enabled.
277
- */
278
- export function notifyUser(message: string): void {
279
- if (__quietMode) return;
280
- try {
281
- console.log(`[m[devthink] ${message}`);
282
- } catch {
283
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
284
- }
285
- }
286
-
287
- // ---------------------------------------------------------------------------
288
- // Account persistence — minimal inline implementation + reuse AccountManager if present
289
- // ---------------------------------------------------------------------------
290
-
291
- function resolveConfigDir(): string {
292
- const env = process.env.OPENCODE_CONFIG_DIR?.trim() ?? "";
293
- if (env) return resolve(env);
294
- return join(homedir(), ".config", "opencode");
295
- }
296
- function resolveAccountsPath(custom?: string): string {
297
- if (custom) return resolve(custom);
298
- return join(resolveConfigDir(), "antigravity-accounts.json");
299
- }
300
-
301
- async function loadAccountsInline(filePath?: string): Promise<Account[]> {
302
- const fp = resolveAccountsPath(filePath);
303
- try {
304
- const content = await fsp.readFile(fp, "utf8");
305
- const parsed = JSON.parse(content);
306
- if (Array.isArray(parsed)) return parsed as Account[];
307
- if (Array.isArray(parsed.accounts)) return parsed.accounts as Account[];
308
- if (parsed.accounts && typeof parsed.accounts === "object") return Object.values(parsed.accounts) as Account[];
309
- return [];
310
- } catch {
311
- return [];
312
- }
313
- }
314
-
315
- async function saveAccountsInline(accounts: Account[], filePath?: string): Promise<void> {
316
- const fp = resolveAccountsPath(filePath);
317
- const dir = dirname(fp);
318
- try {
319
- await fsp.mkdir(dir, { recursive: true, mode: 0o755 });
320
- } catch {
321
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
322
- }
323
- const wrapper = { version: 3, accounts, updatedAt: Date.now() };
324
- const tmp = `${fp}.tmp.${randomInt(100000, 999999)}`;
325
- try {
326
- await fsp.writeFile(tmp, JSON.stringify(wrapper, null, 2), { mode: 0o600 });
327
- await fsp.rename(tmp, fp);
328
- try {
329
- await fsp.chmod(fp, 0o600);
330
- } catch {
331
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
332
- }
333
- } catch {
334
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
335
- }
336
- }
337
-
338
- // Minimal AccountManager fallback that reuses inline loader + round-robin
339
- class InlineAccountManager {
340
- private accounts: Account[] = [];
341
- private rotationIndex = 0;
342
- private loaded = false;
343
- private filePath: string;
344
-
345
- constructor(filePath?: string) {
346
- this.filePath = resolveAccountsPath(filePath);
347
- }
348
-
349
- async load(): Promise<void> {
350
- if (this.loaded) return;
351
- this.accounts = await loadAccountsInline(this.filePath);
352
- this.loaded = true;
353
- }
354
- async save(): Promise<void> {
355
- await saveAccountsInline(this.accounts, this.filePath);
356
- }
357
-
358
- list(): Account[] {
359
- return [...this.accounts];
360
- }
361
-
362
- async getNextAccount(strategy: "round-robin" | "sticky" = "round-robin"): Promise<Account | null> {
363
- await this.load();
364
- const healthy = this.accounts.filter(
365
- (a) => !a.disabled && (!a.quotaExhaustedUntil || Date.now() >= a.quotaExhaustedUntil),
366
- );
367
- if (healthy.length === 0) return this.accounts[0] ?? null;
368
- if (strategy === "sticky" && healthy.length > 0) return healthy[0]!;
369
- const idx = this.rotationIndex % healthy.length;
370
- const acc = healthy[idx]!;
371
- this.rotationIndex = (this.rotationIndex + 1) % healthy.length;
372
- return acc;
373
- }
374
-
375
- async addAccount(a: Account): Promise<void> {
376
- await this.load();
377
- const existingIdx = this.accounts.findIndex((x) => x.email.toLowerCase() === a.email.toLowerCase());
378
- if (existingIdx >= 0) this.accounts[existingIdx] = { ...this.accounts[existingIdx], ...a, updatedAt: Date.now() };
379
- else this.accounts.push({ ...a, createdAt: Date.now(), updatedAt: Date.now() });
380
- await this.save();
381
- }
382
-
383
- async markQuotaExhausted(email: string, durationMs = 20 * 60 * 1000): Promise<void> {
384
- const acc = this.accounts.find((a) => a.email.toLowerCase() === email.toLowerCase());
385
- if (acc) {
386
- acc.quotaExhaustedUntil = Date.now() + durationMs;
387
- await this.save();
388
- }
389
- }
390
-
391
- async refreshAccount(email: string): Promise<{ accessToken?: string } | null> {
392
- // try oauth module if present
393
- try {
394
- const acc = this.accounts.find((a) => a.email.toLowerCase() === email.toLowerCase());
395
- if (!acc?.refreshToken) return null;
396
- if (typeof oauthMod.refreshAccessToken === "function") {
397
- // refresh with the OAuth client the account authenticated with
398
- const identity = resolveOAuthIdentity(identityFromAccountType((acc as any).type));
399
- const tokens = await oauthMod.refreshAccessToken(acc.refreshToken, {
400
- clientId: identity.clientId,
401
- clientSecret: identity.clientSecret,
402
- });
403
- if (tokens?.access_token) {
404
- acc.accessToken = tokens.access_token;
405
- acc.expiry = Date.now() + (tokens.expires_in ?? 3600) * 1000;
406
- await this.save();
407
- return { accessToken: tokens.access_token };
408
- }
409
- }
410
- } catch {
411
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
412
- }
413
- return null;
414
- }
415
- }
416
-
417
- // ---------------------------------------------------------------------------
418
- // Quota — minimal dual pool + shouldSkipAccount
419
- // ---------------------------------------------------------------------------
420
-
421
- /**
422
- * Quota-pool classifier for the inline quota manager.
423
- * v2.1.16 single-owner audit: GENUINELY DIVERGENT from models.js
424
- * mapModelToGroup (the ROUTING classifier) and therefore kept local —
425
- * mapModelToGroup routes non-3.x gemini ids (e.g. gemini-2.5-*) to the
426
- * "antigravity" endpoint family, while this quota view bills EVERY gemini
427
- * model against the "gemini-cli" Cloud Code Assist quota pool. Merging the
428
- * two would change which pool a gemini-2.5 quota exhaustion marks, so the
429
- * divergence is documented instead of merged.
430
- */
431
- function mapModelToQuotaGroup(model: string): string {
432
- const lower = (model ?? "").toLowerCase();
433
- if (lower.includes("claude") || lower.includes("gpt-oss")) return "antigravity";
434
- if (lower.includes("gemini")) return "gemini-cli";
435
- return "antigravity";
436
- }
437
-
438
- class InlineQuotaManager {
439
- private exhausted: Map<string, { until: number; group?: string }> = new Map();
440
-
441
- shouldSkipAccount(email: string, model: string): boolean {
442
- const key = `${email.toLowerCase()}:${mapModelToQuotaGroup(model)}`;
443
- const entry = this.exhausted.get(key) ?? this.exhausted.get(email.toLowerCase());
444
- if (!entry) return false;
445
- if (Date.now() >= entry.until) {
446
- this.exhausted.delete(key);
447
- this.exhausted.delete(email.toLowerCase());
448
- return false;
449
- }
450
- return true;
451
- }
452
-
453
- markExhausted(email: string, model: string, durationMs = 20 * 60 * 1000, group?: string): void {
454
- const g = group ?? mapModelToQuotaGroup(model);
455
- this.exhausted.set(`${email.toLowerCase()}:${g}`, { until: Date.now() + durationMs, group: g });
456
- }
457
-
458
- isQuotaError(status: number, bodyText?: string): boolean {
459
- if (status === 429) return true;
460
- if (!bodyText) return false;
461
- const lower = bodyText.toLowerCase();
462
- return (
463
- lower.includes("quota") ||
464
- lower.includes("exhausted") ||
465
- lower.includes("limit") ||
466
- lower.includes("resource_exhausted")
467
- );
468
- }
469
- }
470
-
471
- // ---------------------------------------------------------------------------
472
- // Streaming helpers — SSE tolerant parser + OpenAI/Anthropic transformers minimal
473
- // ---------------------------------------------------------------------------
474
-
475
- const SSE_DONE = "[DONE]";
476
-
477
- function createSSEParser() {
478
- let buffer = "";
479
- return {
480
- parseChunk(chunk: string): { data: string; event?: string | undefined }[] {
481
- buffer += chunk;
482
- const events: { data: string; event?: string | undefined }[] = [];
483
- let idx;
484
- while ((idx = buffer.indexOf("\n\n")) >= 0) {
485
- const block = buffer.slice(0, idx);
486
- buffer = buffer.slice(idx + 2);
487
- const lines = block.split("\n");
488
- let data = "";
489
- let event = "";
490
- for (const line of lines) {
491
- if (line.startsWith("data:")) data += (data ? "\n" : "") + line.slice(5).trimStart();
492
- else if (line.startsWith("event:")) event = line.slice(6).trim();
493
- else if (line.trim() === "") continue;
494
- else data += (data ? "\n" : "") + line;
495
- }
496
- if (data) events.push({ data, event: event || undefined });
497
- }
498
- return events;
499
- },
500
- flush(): { data: string; event?: string | undefined }[] {
501
- if (!buffer.trim()) return [];
502
- const b = buffer;
503
- buffer = "";
504
- const lines = b.split("\n");
505
- let data = "";
506
- let event = "";
507
- for (const line of lines) {
508
- if (line.startsWith("data:")) data += (data ? "\n" : "") + line.slice(5).trimStart();
509
- else if (line.startsWith("event:")) event = line.slice(6).trim();
510
- else if (line.trim()) data += (data ? "\n" : "") + line;
511
- }
512
- return data ? [{ data, event: event || undefined }] : [];
513
- },
514
- };
515
- }
516
-
517
- function normalizeGeminiPayload(raw: string): any[] {
518
- const out: any[] = [];
519
- const trimmed = raw.trim();
520
- if (!trimmed || trimmed === SSE_DONE) return out;
521
- let parsed: any;
522
- try {
523
- parsed = JSON.parse(trimmed);
524
- } catch {
525
- // sometimes wrapped as "{"response":...}"? try tolerant
526
- return out;
527
- }
528
- // cloudcode-pa returns {response: {candidates:[{content:{parts:[...]}}]}} or direct
529
- const unwrap = (obj: any): any[] => {
530
- if (!obj) return [];
531
- if (obj.response) return unwrap(obj.response);
532
- if (Array.isArray(obj.candidates)) return obj.candidates;
533
- if (obj.candidates) return unwrap(obj.candidates);
534
- if (obj.content) return [obj];
535
- return [obj];
536
- };
537
- const candidates = unwrap(parsed);
538
- for (const cand of candidates) {
539
- const content = cand.content ?? cand;
540
- const parts = content.parts ?? content.content?.parts ?? [];
541
- if (!Array.isArray(parts)) continue;
542
- for (const part of parts) out.push(part);
543
- }
544
- // also handle case where parsed is already part
545
- if (candidates.length === 0 && (parsed.text || parsed.functionCall || parsed.thought)) out.push(parsed);
546
- return out;
547
- }
548
-
549
- function transformToOpenAIChunk(parts: any[], model: string, isFirst: boolean, isLast: boolean): any {
550
- // minimal OpenAI delta
551
- let text = "";
552
- const toolCalls: any[] = [];
553
- let thought = "";
554
- for (const p of parts) {
555
- if (p.text) text += p.text;
556
- if (p.functionCall)
557
- toolCalls.push({
558
- id: `call_${fnv1a32Hex(JSON.stringify(p.functionCall)).slice(0, 8)}`,
559
- type: "function",
560
- function: { name: p.functionCall.name, arguments: JSON.stringify(p.functionCall.args ?? {}) },
561
- });
562
- if (p.thought) thought += p.thought;
563
- }
564
- const delta: any = { role: isFirst ? "assistant" : undefined, content: text || undefined };
565
- if (toolCalls.length > 0) delta.tool_calls = toolCalls;
566
- const chunk: any = {
567
- id: `chatcmpl-${fnv1a32Hex(model + Date.now()).slice(0, 12)}`,
568
- object: "chat.completion.chunk",
569
- created: Math.floor(Date.now() / 1000),
570
- model,
571
- choices: [{ index: 0, delta, finish_reason: isLast ? "stop" : null }],
572
- };
573
- return chunk;
574
- }
575
-
576
- // ---------------------------------------------------------------------------
577
- // Main plugin class — bypass Project ID exactly as Gemini CLI
578
- // ---------------------------------------------------------------------------
579
-
580
- /**
581
- * Extracts the model id from a Gemini-style URL
582
- * (.../models/{model}:method?...) using a linear scan — no regex on
583
- * uncontrolled input. Returns undefined when the URL carries no model.
584
- */
585
- function extractModelFromUrl(urlStr: string): string | undefined {
586
- const marker = "/models/";
587
- const i = urlStr.indexOf(marker);
588
- if (i < 0) return undefined;
589
- const rest = urlStr.slice(i + marker.length);
590
- const colon = rest.indexOf(":");
591
- const query = rest.indexOf("?");
592
- let end = colon >= 0 ? colon : query >= 0 ? query : rest.length;
593
- if (end < 0) end = rest.length;
594
- const m = rest.slice(0, end);
595
- if (!m) return undefined;
596
- try {
597
- return decodeURIComponent(m);
598
- } catch {
599
- return m;
600
- }
601
- }
602
-
603
- export class AntigravityPlugin {
604
- private accountManager: any;
605
- private quotaManager: InlineQuotaManager;
606
- private options: PluginOptions;
607
- private version: string = ANTIGRAVITY_VERSION_FALLBACK;
608
- private initialized = false;
609
-
610
- constructor(options: PluginOptions = {}) {
611
- this.options = { claude_tool_hardening: true, keep_thinking: false, soft_quota_threshold_percent: 90, ...options };
612
- __quietMode = !!this.options.quiet_mode;
613
- this.quotaManager = new InlineQuotaManager();
614
- // try load external AccountManager for advanced persistence, fallback inline
615
- try {
616
- // will be lazy initialized in init()
617
- this.accountManager = new InlineAccountManager();
618
- } catch {
619
- this.accountManager = new InlineAccountManager();
620
- }
621
- }
622
-
623
- async init(): Promise<void> {
624
- if (this.initialized) return;
625
- try {
626
- this.version = await getVersion().catch(() => ANTIGRAVITY_VERSION_FALLBACK);
627
- } catch {
628
- this.version = ANTIGRAVITY_VERSION_FALLBACK;
629
- }
630
-
631
- // attempt to use real AccountManager if exists (has richer refresh)
632
- try {
633
- const ExtMgr = (accountsMod as any).AccountManager;
634
- if (ExtMgr) {
635
- const mgr = new ExtMgr();
636
- await (mgr as any).load?.().catch(() => {});
637
- // prefer the richer auth module manager when it has accounts or robin-hood selection
638
- if (((mgr as any).accounts && (mgr as any).accounts.length > 0) || typeof (mgr as any).getNext === "function")
639
- this.accountManager = mgr;
640
- else {
641
- // keep inline but copy accounts if external has
642
- await this.accountManager.load().catch(() => {});
643
- }
644
- } else {
645
- await this.accountManager.load().catch(() => {});
646
- }
647
- } catch {
648
- await this.accountManager.load().catch(() => {});
649
- }
650
-
651
- this.initialized = true;
652
- }
653
-
654
- private getUserAgent(): string {
655
- try {
656
- return getDynamicUserAgent(this.version);
657
- } catch {
658
- return `antigravity/${this.version} linux/x64`;
659
- }
660
- }
661
-
662
- private shouldHandleModel(model: string): boolean {
663
- if (!model) return false;
664
- const lower = model.toLowerCase();
665
- return (
666
- lower.includes("antigravity") ||
667
- lower.includes("gemini") ||
668
- lower.includes("claude") ||
669
- lower.includes("gpt-oss") ||
670
- MODELS_2026_PROVIDER.some((m) => lower.includes(m.toLowerCase()))
671
- );
672
- }
673
-
674
- private async getAccountForModel(model: string): Promise<Account | null> {
675
- const preferCliFirst = this.options.cli_first ?? false;
676
- const strategy = this.options.rotation_strategy ?? "round-robin";
677
- // soft quota: pass threshold through to managers that support it (auth.js getNext)
678
- const softThreshold = this.options.soft_quota_threshold_percent ?? 90;
679
- const pickNext = async (strat: string): Promise<any | null> => {
680
- try {
681
- if (typeof (this.accountManager as any).getNext === "function")
682
- return await (this.accountManager as any).getNext(strat, softThreshold);
683
- } catch {
684
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
685
- }
686
- try {
687
- return (await (this.accountManager as any).getNextAccount?.(strat)) ?? null;
688
- } catch {
689
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
690
- }
691
- try {
692
- return (await (this.accountManager as any).getNextAccount(strat)) ?? null;
693
- } catch {
694
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
695
- }
696
- return null;
697
- };
698
- // dual quota: if cli_first and gemini model, prefer gemini-cli pool first (preserve antigravity for claude)
699
- // observer implements as round-robin with skip
700
- let lastErr: any = null;
701
- for (let i = 0; i < 10; i++) {
702
- try {
703
- const candidate = await pickNext(strategy);
704
- if (!candidate) break;
705
- if (this.quotaManager.shouldSkipAccount(candidate.email, model)) continue;
706
-
707
- // cli_first: if gemini and candidate type is antigravity, try to find gemini-cli first? simple heuristic
708
- if (preferCliFirst && model.toLowerCase().includes("gemini")) {
709
- // if candidate is claude-only? we still allow but prefer gemini-cli: skip antigravity if another healthy exists
710
- const isAntigravityOnly = candidate.type === "antigravity" && isGeminiCLIOnlyModel(model);
711
- // If gemini-cli only model, we should not skip antigravity but note that fallback project would 403 — try next once
712
- if (isAntigravityOnly) {
713
- // look ahead one more account in case multi-account
714
- const nextCandidate = await pickNext("round-robin");
715
- if (nextCandidate && !this.quotaManager.shouldSkipAccount(nextCandidate.email, model)) {
716
- return nextCandidate as Account;
717
- }
718
- }
719
- }
720
-
721
- return candidate as Account;
722
- } catch (e) {
723
- lastErr = e;
724
- continue;
725
- }
726
- }
727
- // fallback any
728
- try {
729
- const anyAcc = await pickNext(strategy);
730
- return anyAcc as Account | null;
731
- } catch {
732
- return null;
733
- }
734
- }
735
-
736
- private async refreshIfNeeded(account: Account): Promise<string> {
737
- if (account.accessToken && account.expiry && Date.now() < account.expiry - 5 * 60 * 1000)
738
- return account.accessToken;
739
- try {
740
- const refreshed = (await (this.accountManager as any).refreshAccount?.(account.email).catch(() => null)) ?? null;
741
- if (refreshed?.accessToken) return refreshed.accessToken;
742
- if ((refreshed as any)?.access_token) return (refreshed as any).access_token;
743
- } catch {
744
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
745
- }
746
- try {
747
- if (typeof oauthMod.refreshAccessToken === "function" && account.refreshToken) {
748
- // refresh with the OAuth client the account authenticated with
749
- const identity = resolveOAuthIdentity(
750
- identityFromAccountType((account as any).type ?? (account as any).oauthClientKey),
751
- );
752
- const tokens = await oauthMod.refreshAccessToken(account.refreshToken, {
753
- clientId: identity.clientId,
754
- clientSecret: identity.clientSecret,
755
- });
756
- if (tokens?.access_token) {
757
- account.accessToken = tokens.access_token;
758
- account.expiry = Date.now() + (tokens.expires_in ?? 3600) * 1000;
759
- await this.accountManager.save?.().catch(() => {});
760
- return tokens.access_token;
761
- }
762
- }
763
- } catch {
764
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
765
- }
766
- if (account.accessToken) return account.accessToken;
767
- throw new Error(`no access token for ${account.email}`);
768
- }
769
-
770
- /**
771
- * handleFetch — intercepta fetch do opencode provider google.
772
- * Observer third-person: faz exatamente como Gemini CLI:
773
- * - se account.projectId ausente, chama resolveProjectId que faz loadCodeAssist SEM project, onboardUser se necessário, nunca exige project do usuário.
774
- * - wrapping {model, project, request} direto cloudcode-pa, sem localhost v1 proxy.
775
- * - cascade 403/404/5xx, strip x-goog-user-project, skip sandbox para gemini-3.6/3.5/preview.
776
- * - dual quota + cli_first + pid_offset.
777
- */
778
- async handleFetch(originalFetch: typeof fetch, input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
779
- await this.init();
780
-
781
- const urlStr =
782
- typeof input === "string" ? input : input instanceof URL ? input.toString() : ((input as Request).url ?? "");
783
- const method = (
784
- init?.method ??
785
- (typeof input !== "string" && !(input instanceof URL) ? (input as Request).method : "POST") ??
786
- "POST"
787
- ).toUpperCase();
788
- const bodyStr = (init?.body as string) ?? "";
789
-
790
- // only intercept POST to google / generative or our own transformed calls
791
- // opencode typically calls fetch to provider endpoint; body contains model
792
- let parsedBody: any = null;
793
- let model: string | undefined;
794
- let messages: any[] | undefined;
795
- let tools: any[] | undefined;
796
- let system: any | undefined;
797
- let thinkingLevel: string | undefined;
798
- let generationConfig: any | undefined;
799
- let isAlreadyWrapped = false;
800
-
801
- try {
802
- if (bodyStr) {
803
- parsedBody = JSON.parse(bodyStr);
804
- model = parsedBody?.model ?? parsedBody?.request?.model ?? parsedBody?.modelId ?? "";
805
- // Gemini-NATIVE SDK body (@ai-sdk/google and friends): {contents, systemInstruction?,
806
- // tools?, toolConfig?, generationConfig?} at the ROOT with the model ONLY in the URL.
807
- // Normalize in place into the wrapped {model, project, request} shape so the
808
- // isAlreadyWrapped path below preserves the native request verbatim.
809
- if (
810
- !Array.isArray(parsedBody?.messages) &&
811
- Array.isArray(parsedBody?.contents) &&
812
- parsedBody?.request === undefined
813
- ) {
814
- const urlModel = extractModelFromUrl(urlStr);
815
- const native: any = parsedBody;
816
- parsedBody.request = {
817
- contents: native.contents,
818
- ...(native.systemInstruction !== undefined ? { systemInstruction: native.systemInstruction } : {}),
819
- ...(native.tools !== undefined ? { tools: native.tools } : {}),
820
- ...(native.toolConfig !== undefined ? { toolConfig: native.toolConfig } : {}),
821
- ...(native.generationConfig !== undefined ? { generationConfig: native.generationConfig } : {}),
822
- };
823
- parsedBody.project = "__devthink_native__"; // marker so the wrapped check below passes
824
- if (!parsedBody.model) parsedBody.model = urlModel ?? "";
825
- model = parsedBody.model;
826
- }
827
- // detect if already in cloudcode envelope {model, project, request}
828
- if (parsedBody?.project && parsedBody?.request && parsedBody?.model) {
829
- isAlreadyWrapped = true;
830
- model = parsedBody.model;
831
- } else {
832
- messages = parsedBody?.messages ?? parsedBody?.request?.contents ?? undefined;
833
- tools = parsedBody?.tools ?? parsedBody?.request?.tools ?? undefined;
834
- system =
835
- parsedBody?.system ?? parsedBody?.systemInstruction ?? parsedBody?.request?.systemInstruction ?? undefined;
836
- thinkingLevel =
837
- parsedBody?.thinkingLevel ?? parsedBody?.thinking_level ?? parsedBody?.request?.thinkingLevel ?? undefined;
838
- generationConfig = parsedBody?.generationConfig ?? parsedBody?.request?.generationConfig ?? undefined;
839
- }
840
- }
841
- } catch {
842
- // not json, skip intercept
843
- }
844
-
845
- // if not our model, passthrough
846
- if (!isAlreadyWrapped && (!model || !this.shouldHandleModel(model))) {
847
- return originalFetch(input, init);
848
- }
849
-
850
- const effectiveModel = String(model ?? "").trim();
851
- if (!effectiveModel) return originalFetch(input, init);
852
-
853
- // account selection dual quota
854
- const selectedAccount = await this.getAccountForModel(effectiveModel);
855
- if (!selectedAccount) {
856
- return new Response(
857
- JSON.stringify({
858
- error: `no antigravity accounts available for model ${effectiveModel} — run opencode auth login`,
859
- }),
860
- { status: 401, headers: { "Content-Type": "application/json" } },
861
- );
862
- }
863
- let account: Account = selectedAccount;
864
-
865
- let accessToken: string;
866
- try {
867
- accessToken = await this.refreshIfNeeded(account);
868
- } catch (e: any) {
869
- return new Response(JSON.stringify({ error: `failed to refresh token for ${account.email}: ${e.message}` }), {
870
- status: 401,
871
- });
872
- }
873
-
874
- // bypass total Project ID — exactly Gemini CLI.
875
- // NOTE: the FALLBACK project id is treated as "not discovered": accounts
876
- // provisioned with the fallback never ran loadCodeAssist/onboardUser, so we
877
- // resolve here (loadCodeAssist -> onboardUser free-tier -> load) to discover
878
- // the account's REAL cloudaicompanionProject (verified live 2026-08-26: the
879
- // fallback project yields 403 PERMISSION_DENIED on streamGenerateContent).
880
- let projectId = (account as any).projectId?.trim() ?? "";
881
- if (!projectId || projectId === FALLBACK_PROJECT_ID) {
882
- try {
883
- const resolved = await resolveProjectId({
884
- accessToken,
885
- cachedProjectId: undefined,
886
- email: account.email,
887
- metadataSaver: async (pid: string) => {
888
- (account as any).projectId = pid;
889
- // save to manager
890
- try {
891
- await this.accountManager.save?.();
892
- } catch {
893
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
894
- }
895
- },
896
- userAgent: this.getUserAgent(),
897
- });
898
- projectId = resolved.projectId;
899
- } catch (e: any) {
900
- // fallback still returns project id, but if throw -> use blinded fallback with warning
901
- projectId = FALLBACK_PROJECT_ID;
902
- console.warn(
903
- `[m[devthink] resolveProjectId failed for ${account.email}, using fallback ${FALLBACK_PROJECT_ID}: ${e.message}`,
904
- );
905
- }
906
- }
907
-
908
- // transform request if not already wrapped
909
- // google_search_enabled:"auto" → grounding only for Gemini (non-Claude) models
910
- const searchOpt = this.options.google_search_enabled ?? true;
911
- const searchEnabled =
912
- typeof searchOpt === "string"
913
- ? searchOpt === "auto"
914
- ? effectiveModel.toLowerCase().includes("gemini") && !effectiveModel.toLowerCase().includes("claude")
915
- : false
916
- : !!searchOpt;
917
-
918
- let buildOpts: BuildOptions;
919
- if (isAlreadyWrapped) {
920
- // already {model, project, request} — ensure project is our resolved one
921
- const existingRequest = parsedBody.request ?? {};
922
- buildOpts = {
923
- model: effectiveModel,
924
- messages: existingRequest.contents ? [{ role: "user", content: "" }] : messages, // dummy, will be overridden since we already have request
925
- tools,
926
- system,
927
- thinkingLevel,
928
- generationConfig,
929
- accessToken,
930
- projectId,
931
- userAgent: this.getUserAgent(),
932
- antigravityVersion: this.version,
933
- pidOffsetEnabled: this.options.pid_offset_enabled ?? false,
934
- googleSearchEnabled: searchEnabled,
935
- claudeToolHardening: this.options.claude_tool_hardening,
936
- keepThinking: this.options.keep_thinking,
937
- };
938
- // override inner via direct usage of parsedBody.request for preserving exact content if provided
939
- // but still rebuild to ensure deterministic ids
940
- buildOpts.messages = undefined; // we will inject directly below
941
- } else {
942
- buildOpts = {
943
- model: effectiveModel,
944
- messages,
945
- tools,
946
- system,
947
- thinkingLevel,
948
- generationConfig,
949
- accessToken,
950
- projectId,
951
- userAgent: this.getUserAgent(),
952
- antigravityVersion: this.version,
953
- pidOffsetEnabled: this.options.pid_offset_enabled ?? false,
954
- googleSearchEnabled: searchEnabled,
955
- claudeToolHardening: this.options.claude_tool_hardening,
956
- keepThinking: this.options.keep_thinking,
957
- };
958
- }
959
-
960
- // image generation: strip tools and merge image gen config into the request
961
- if (isImageGenModel(effectiveModel)) {
962
- buildOpts.tools = [];
963
- }
964
-
965
- // Decide whether to use gemini-cli builder (prod-only) or antigravity generic
966
- const isCliOnly = isGeminiCLIOnlyModel(effectiveModel) || shouldSkipSandbox(effectiveModel);
967
- const builder =
968
- (this.options.cli_first &&
969
- effectiveModel.toLowerCase().includes("gemini") &&
970
- !effectiveModel.toLowerCase().includes("claude")) ||
971
- isCliOnly
972
- ? buildGeminiCLIRequest
973
- : buildAntigravityRequest;
974
-
975
- let buildResult: BuildResult;
976
- try {
977
- buildResult = builder(buildOpts);
978
-
979
- if (isImageGenModel(effectiveModel)) {
980
- buildResult.body.request.generationConfig = {
981
- ...(buildResult.body.request?.generationConfig ?? {}),
982
- ...buildImageGenConfig(),
983
- };
984
- }
985
-
986
- // If original was already wrapped (or Gemini-native normalized above),
987
- // preserve its request content but use our project and ids.
988
- if (isAlreadyWrapped && parsedBody?.request) {
989
- // Keep original contents etc to avoid double transformation
990
- const originalReq = parsedBody.request;
991
- const builtReq = buildResult.body.request ?? {};
992
- // generationConfig: the USER's fields win (never force parameters) —
993
- // only the thinking config we synthesized is merged on top.
994
- const builtGen: Record<string, any> = builtReq.generationConfig ?? {};
995
- const origGen: Record<string, any> = originalReq.generationConfig ?? {};
996
- const mergedGen: Record<string, any> = { ...origGen };
997
- for (const [k, v] of Object.entries(builtGen)) {
998
- if (k === "thinkingConfig") {
999
- mergedGen.thinkingConfig = { ...((origGen.thinkingConfig as Record<string, any>) ?? {}), ...v };
1000
- } else if (mergedGen[k] === undefined) {
1001
- mergedGen[k] = v;
1002
- }
1003
- }
1004
- // tools: keep the original declarations, add only what the builder
1005
- // injected (googleSearch) when missing.
1006
- const mergedTools: any[] = Array.isArray(originalReq.tools) ? [...originalReq.tools] : [];
1007
- if (Array.isArray(builtReq.tools)) {
1008
- for (const t of builtReq.tools) {
1009
- if (t && t.googleSearch) {
1010
- if (!mergedTools.some((x) => x && x.googleSearch)) mergedTools.push(t);
1011
- } else if (t && Array.isArray(t.functionDeclarations) && t.functionDeclarations.length > 0) {
1012
- if (!mergedTools.some((x) => x && x.functionDeclarations)) mergedTools.push(t);
1013
- }
1014
- }
1015
- }
1016
- buildResult.body.request = {
1017
- ...originalReq,
1018
- generationConfig: mergedGen,
1019
- tools: mergedTools.length > 0 ? mergedTools : originalReq.tools,
1020
- };
1021
- // update model/project remains
1022
- buildResult.body.model = effectiveModel;
1023
- buildResult.body.project = projectId;
1024
- }
1025
- } catch (e: any) {
1026
- return new Response(JSON.stringify({ error: `failed to build antigravity request: ${e.message}` }), {
1027
- status: 500,
1028
- });
1029
- }
1030
-
1031
- // cascade fetch with retry 403/404/5xx + strip forbidden headers
1032
- const exec = async (): Promise<Response> => {
1033
- let lastRes: Response | null = null;
1034
- let lastError: Error | null = null;
1035
-
1036
- // attempt up to 2 account rotations on 403/429
1037
- for (let accountAttempt = 0; accountAttempt < 2; accountAttempt++) {
1038
- try {
1039
- const response = await fetchWithCascade(buildResult, { timeoutMs: CONTENT_TIMEOUT_MS });
1040
-
1041
- // handle quota exhausted
1042
- if (response.status === 429 || (response.headers.get("content-type")?.includes("json") && false)) {
1043
- // read body text for quota check but need clone
1044
- const cloned = response.clone();
1045
- const txt = await cloned.text().catch(() => "");
1046
- if (this.quotaManager.isQuotaError(response.status, txt)) {
1047
- this.quotaManager.markExhausted(account.email, effectiveModel, 60 * 60 * 1000);
1048
- await (this.accountManager as any).markQuotaExhausted?.(account.email, 60 * 60 * 1000).catch(() => {});
1049
- notifyUser(`account ${account.email} rate-limited, rotating`);
1050
- // soft quota: percent-bearing bodies mark a shorter cooldown window
1051
- const low = txt.toLowerCase();
1052
- if (low.includes("percent") || low.includes("%")) {
1053
- const ttlMinOpt = this.options.soft_quota_cache_ttl_minutes;
1054
- const softMs = (ttlMinOpt === "auto" || ttlMinOpt === undefined ? 15 : ttlMinOpt) * 60 * 1000;
1055
- (this.accountManager as any).markSoftQuota?.(account.email, softMs);
1056
- }
1057
- // rotate account
1058
- const nextAcc = await this.getAccountForModel(effectiveModel);
1059
- if (nextAcc && nextAcc.email !== account.email) {
1060
- account = nextAcc;
1061
- try {
1062
- accessToken = await this.refreshIfNeeded(account);
1063
- } catch {
1064
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1065
- }
1066
- // re-resolve projectId if missing for new account
1067
- let newPid = (account as any).projectId?.trim() ?? "";
1068
- if (!newPid) {
1069
- try {
1070
- const resolved = await resolveProjectId({
1071
- accessToken,
1072
- email: account.email,
1073
- metadataSaver: async (pid: string) => {
1074
- (account as any).projectId = pid;
1075
- try {
1076
- await this.accountManager.save?.();
1077
- } catch {
1078
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1079
- }
1080
- },
1081
- userAgent: this.getUserAgent(),
1082
- });
1083
- newPid = resolved.projectId;
1084
- } catch {
1085
- newPid = FALLBACK_PROJECT_ID;
1086
- }
1087
- }
1088
- // rebuild with new token/project
1089
- buildOpts.accessToken = accessToken;
1090
- buildOpts.projectId = newPid;
1091
- buildResult = builder(buildOpts);
1092
- continue;
1093
- }
1094
- }
1095
- }
1096
-
1097
- // success or non-retryable error — return response, but transform if needed
1098
- lastRes = response;
1099
-
1100
- // if response is not ok and retryable, cascade already handled inside fetchWithCascade loop — but if final is 403/404 here, try next endpoint account rotation
1101
- if (response.status === 403 || response.status === 404 || response.status >= 500) {
1102
- lastError = new Error(`retryable status ${response.status}`);
1103
- // try next account rotation once
1104
- if (accountAttempt === 0) {
1105
- const nextAcc = await this.getAccountForModel(effectiveModel);
1106
- if (nextAcc && nextAcc.email !== account.email) {
1107
- account = nextAcc;
1108
- try {
1109
- accessToken = await this.refreshIfNeeded(account);
1110
- } catch {
1111
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1112
- }
1113
- buildOpts.accessToken = accessToken;
1114
- buildOpts.projectId = (account as any).projectId ?? projectId;
1115
- buildResult = builder(buildOpts);
1116
- continue;
1117
- }
1118
- }
1119
- }
1120
-
1121
- return response;
1122
- } catch (e: any) {
1123
- lastError = e instanceof Error ? e : new Error(String(e));
1124
- // try next account if error retryable
1125
- if (accountAttempt === 0) {
1126
- const nextAcc = await this.getAccountForModel(effectiveModel);
1127
- if (nextAcc && nextAcc.email !== account.email) {
1128
- account = nextAcc;
1129
- try {
1130
- accessToken = await this.refreshIfNeeded(account);
1131
- } catch {
1132
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1133
- }
1134
- buildOpts.accessToken = accessToken;
1135
- buildOpts.projectId = (account as any).projectId ?? projectId;
1136
- buildResult = builder(buildOpts);
1137
- continue;
1138
- }
1139
- }
1140
- }
1141
- }
1142
-
1143
- if (lastRes) return lastRes;
1144
- throw lastError ?? new Error("all account attempts failed");
1145
- };
1146
-
1147
- let response: Response | null = null;
1148
- try {
1149
- response = await exec();
1150
- } catch (e: any) {
1151
- // session recovery: classify, notify, sanitize messages, retry once
1152
- const errType = detectFetchErrorType(e);
1153
- let retried = false;
1154
- if (errType !== "unknown") {
1155
- notifyUser(`recoverable ${errType} error, retrying once`);
1156
- try {
1157
- const recoveredMessages = fetchSessionRecovery(errType, messages);
1158
- if (Array.isArray(recoveredMessages)) messages = recoveredMessages;
1159
- if (!isAlreadyWrapped) {
1160
- buildOpts.messages = messages;
1161
- buildResult = builder(buildOpts);
1162
- }
1163
- response = await exec();
1164
- retried = true;
1165
- } catch {
1166
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1167
- }
1168
- }
1169
- if (!retried) {
1170
- // try fallback to other pool if cli_first enabled
1171
- if (this.options.cli_first && !isCliOnly) {
1172
- try {
1173
- const fallbackBuild = buildGeminiCLIRequest({
1174
- ...buildOpts,
1175
- endpointOverrides: [CODE_ASSIST_ENDPOINTS.PROD],
1176
- });
1177
- response = await fetchWithCascade(fallbackBuild);
1178
- } catch {
1179
- return new Response(JSON.stringify({ error: `antigravity request failed after retries: ${e.message}` }), {
1180
- status: 500,
1181
- headers: { "Content-Type": "application/json" },
1182
- });
1183
- }
1184
- } else {
1185
- return new Response(JSON.stringify({ error: `antigravity request failed after retries: ${e.message}` }), {
1186
- status: 500,
1187
- headers: { "Content-Type": "application/json" },
1188
- });
1189
- }
1190
- }
1191
- }
1192
-
1193
- if (!response) throw new Error("antigravity request failed: no response");
1194
-
1195
- // transform SSE to OpenAI-compatible if needed — detect client expects event-stream?
1196
- const contentType = response.headers.get("content-type") ?? "";
1197
- // Only transform SUCCESS responses as SSE — Google sends 4xx/5xx error JSON
1198
- // with Content-Type text/event-stream too (verified live), so status alone
1199
- // must gate the transform or the real error message gets swallowed into an
1200
- // empty chunk and the user never sees "not available in your location".
1201
- const isSSE =
1202
- response.ok &&
1203
- (contentType.includes("text/event-stream") ||
1204
- response.url.includes("alt=sse") ||
1205
- effectiveModel.includes("gemini") ||
1206
- effectiveModel.includes("claude"));
1207
-
1208
- // If caller originally asked for OpenAI SSE (common in opencode), we transform cloudcode SSE → OpenAI SSE lines
1209
- // Observer notes: return text/event-stream with data: {...}\n\n lines
1210
- if (isSSE && response.body) {
1211
- const isAnthropic = parsedBody?.anthropic_version !== undefined || urlStr.toLowerCase().includes("anthropic");
1212
-
1213
- const parser = createSSEParser();
1214
- const decoder = new TextDecoder();
1215
- const encoder = new TextEncoder();
1216
- // keep_thinking: emit reasoning parts; otherwise drop thought-only parts
1217
- const keepThinking = !!this.options.keep_thinking;
1218
- const sessionIdForPreserve = buildResult.sessionId;
1219
-
1220
- const originalStream = response.body as ReadableStream<Uint8Array>;
1221
-
1222
- /** Emits one raw Gemini part: preserves thinking signatures, honors keepThinking. */
1223
- const emitPart = (part: any): boolean => {
1224
- if (!part) return false;
1225
- if (part.thought === true || typeof part.thoughtSignature === "string") {
1226
- const thinkingText =
1227
- typeof part.thought === "string" ? part.thought : typeof part.text === "string" ? part.text : "";
1228
- if (thinkingText && typeof part.thoughtSignature === "string") {
1229
- preserveValidatedSignature(thinkingText, part.thoughtSignature, {
1230
- sessionId: sessionIdForPreserve,
1231
- modelId: effectiveModel,
1232
- });
1233
- }
1234
- if (!keepThinking) return false; // drop reasoning-only part unless keep_thinking
1235
- }
1236
- return true;
1237
- };
1238
-
1239
- const transformedStream = new ReadableStream({
1240
- async start(controller) {
1241
- const reader = originalStream.getReader();
1242
- let first = true;
1243
- try {
1244
- while (true) {
1245
- const { done, value } = await reader.read();
1246
- if (done) break;
1247
- const text = decoder.decode(value, { stream: true });
1248
- const events = parser.parseChunk(text);
1249
- for (const ev of events) {
1250
- const payloads = normalizeGeminiPayload(ev.data);
1251
- for (const part of payloads) {
1252
- if (!emitPart(part)) continue;
1253
- const chunkObj = transformToOpenAIChunk([part], effectiveModel, first, false);
1254
- first = false;
1255
- const line = `data: ${JSON.stringify(chunkObj)}\n\n`;
1256
- controller.enqueue(encoder.encode(line));
1257
- }
1258
- // if event data is [DONE] close
1259
- if (ev.data.trim() === SSE_DONE) {
1260
- controller.enqueue(encoder.encode(`data: ${SSE_DONE}\n\n`));
1261
- }
1262
- }
1263
- }
1264
- // flush
1265
- const remaining = parser.flush();
1266
- for (const ev of remaining) {
1267
- const payloads = normalizeGeminiPayload(ev.data);
1268
- for (const part of payloads) {
1269
- if (!emitPart(part)) continue;
1270
- const chunkObj = transformToOpenAIChunk([part], effectiveModel, first, false);
1271
- first = false;
1272
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunkObj)}\n\n`));
1273
- }
1274
- }
1275
- // final chunk with stop
1276
- const finalChunk = {
1277
- id: `chatcmpl-${fnv1a32Hex(effectiveModel + Date.now()).slice(0, 12)}`,
1278
- object: "chat.completion.chunk",
1279
- created: Math.floor(Date.now() / 1000),
1280
- model: effectiveModel,
1281
- choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
1282
- };
1283
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(finalChunk)}\n\n`));
1284
- controller.enqueue(encoder.encode(`data: ${SSE_DONE}\n\n`));
1285
- controller.close();
1286
- } catch (e) {
1287
- controller.error(e);
1288
- }
1289
- },
1290
- });
1291
-
1292
- return new Response(transformedStream, {
1293
- status: response.status,
1294
- headers: {
1295
- "Content-Type": "text/event-stream",
1296
- "Cache-Control": "no-cache",
1297
- Connection: "keep-alive",
1298
- "X-Antigravity-Model": effectiveModel,
1299
- "X-Antigravity-Project": projectId,
1300
- },
1301
- });
1302
- }
1303
-
1304
- // non-streaming: try to transform JSON to OpenAI-compatible
1305
- try {
1306
- const text = await response.text();
1307
- let json: any;
1308
- try {
1309
- json = JSON.parse(text);
1310
- } catch {
1311
- json = { text };
1312
- }
1313
-
1314
- // if already OpenAI shape, just return
1315
- if (json.choices || json.content) {
1316
- return new Response(JSON.stringify(json), {
1317
- status: response.status,
1318
- headers: { "Content-Type": "application/json" },
1319
- });
1320
- }
1321
-
1322
- // unwrap Gemini candidate to text
1323
- const candidates = json.response?.candidates ?? json.candidates ?? [];
1324
- let combinedText = "";
1325
- for (const cand of candidates) {
1326
- const parts = cand.content?.parts ?? [];
1327
- for (const p of parts) if (p.text) combinedText += p.text;
1328
- }
1329
- // if we got text, wrap as OpenAI
1330
- if (combinedText) {
1331
- const openAI = {
1332
- id: `chatcmpl-${fnv1a32Hex(effectiveModel + Date.now()).slice(0, 12)}`,
1333
- object: "chat.completion",
1334
- created: Math.floor(Date.now() / 1000),
1335
- model: effectiveModel,
1336
- choices: [{ index: 0, message: { role: "assistant", content: combinedText }, finish_reason: "stop" }],
1337
- };
1338
- return new Response(JSON.stringify(openAI), { status: 200, headers: { "Content-Type": "application/json" } });
1339
- }
1340
-
1341
- return new Response(JSON.stringify(json), {
1342
- status: response.status,
1343
- headers: { "Content-Type": "application/json" },
1344
- });
1345
- } catch {
1346
- return response;
1347
- }
1348
- }
1349
-
1350
- getPluginDefinition() {
1351
- const self = this;
1352
- return {
1353
- id: PLUGIN_ID,
1354
- version: PLUGIN_VERSION,
1355
- name: "Antigravity Auth Next",
1356
- description:
1357
- "Enable Opencode to authenticate against Antigravity via OAuth — supports models 25/08/2026 (gemini-3.6/3.5/3.1/3/2.5, claude-opus-4-6-thinking, gpt-oss-120b), dual quota, auto-recovery, direct cloudcode-pa (no localhost v1), bypass Project ID as Gemini CLI via loadCodeAssist+onboardUser",
1358
- async onInit() {
1359
- await self.init();
1360
- },
1361
- hooks: {
1362
- "tool.execute.before": async (ctx: any) => ctx,
1363
- "tool.execute.after": async (ctx: any) => ctx,
1364
- fetch: async (input: RequestInfo | URL, init?: RequestInit, next: typeof fetch = fetch) => {
1365
- return self.handleFetch(next, input, init);
1366
- },
1367
- },
1368
- auth: {
1369
- provider: "google",
1370
- /**
1371
- * opencode credential loader (merged from legacy plugin.ts): supplies the
1372
- * active account OAuth credentials to opencode without re-running login.
1373
- */
1374
- async loader() {
1375
- await self.init();
1376
- try {
1377
- const m = self.accountManager;
1378
- const list = (m as any).list?.() ?? (m as any).accounts ?? [];
1379
- const active = Array.isArray(list)
1380
- ? (list.find((a: any) => a?.enabled !== false && a?.disabled !== true) ?? list[0] ?? null)
1381
- : null;
1382
- if (!active) return null;
1383
- let token = active.accessToken ?? active.access_token;
1384
- const expiry = active.expiry ?? active.expiryDate ?? 0;
1385
- if (!token || Date.now() > expiry - 60000) {
1386
- if (typeof (m as any).refreshIfNeeded === "function") {
1387
- try {
1388
- token = await (m as any).refreshIfNeeded(active.email);
1389
- } catch {
1390
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1391
- }
1392
- }
1393
- if (!token && active.refreshToken) {
1394
- try {
1395
- const t = await oauthMod.refreshToken(active.refreshToken);
1396
- token = t.access_token;
1397
- } catch {
1398
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1399
- }
1400
- }
1401
- }
1402
- if (!token) return null;
1403
- return {
1404
- type: "oauth",
1405
- accessToken: token,
1406
- refreshToken: active.refreshToken,
1407
- projectId: active.projectId || FALLBACK_PROJECT_ID,
1408
- email: active.email,
1409
- };
1410
- } catch {
1411
- return null;
1412
- }
1413
- },
1414
- async login() {
1415
- const identity = resolveOAuthIdentity(((self as any).options?.oauth_identity as string) ?? "auto");
1416
- const result = await oauthMod.startOAuthFlow({
1417
- clientId: identity.clientId,
1418
- clientSecret: identity.clientSecret,
1419
- scopes: identity.scopesStr,
1420
- });
1421
- if (!(result as any)?.tokens?.refresh_token && !(result as any)?.refresh_token)
1422
- throw new Error("OAuth failed no refresh_token");
1423
- const tokens = (result as any).tokens ?? result;
1424
- const userInfo = (result as any).userInfo ?? {};
1425
- const email = userInfo.email ?? `user-${Date.now()}@gmail.com`;
1426
- const refresh = tokens.refresh_token ?? (result as any).refresh_token;
1427
- const access = tokens.access_token ?? (result as any).access_token;
1428
- // try add via AccountManager
1429
- try {
1430
- const ExtMgr = (accountsMod as any).AccountManager;
1431
- if (ExtMgr) {
1432
- const mgr = new ExtMgr();
1433
- await mgr.load();
1434
- (await (mgr as any).addAccount?.({
1435
- email,
1436
- refreshToken: refresh,
1437
- accessToken: access,
1438
- oauthClientKey: identity.identity,
1439
- oauthClientConfig: { clientId: identity.clientId, clientSecret: identity.clientSecret },
1440
- type: identity.identity,
1441
- } as any)) ??
1442
- (await (mgr as any)
1443
- .add?.({ email, refreshToken: refresh, accessToken: access } as any)
1444
- .catch(() => {}));
1445
- await mgr.save();
1446
- }
1447
- } catch {
1448
- // fallback inline
1449
- const inline = new InlineAccountManager();
1450
- await inline.load();
1451
- await inline.addAccount({ email, refreshToken: refresh, accessToken: access, type: identity.identity });
1452
- }
1453
- return { email };
1454
- },
1455
- async list() {
1456
- await self.init();
1457
- try {
1458
- const m = self.accountManager;
1459
- return (m as any).list?.() ?? (m as any).accounts ?? [];
1460
- } catch {
1461
- return [];
1462
- }
1463
- },
1464
- async logout(email?: string) {
1465
- try {
1466
- const ExtMgr = (accountsMod as any).AccountManager;
1467
- if (ExtMgr) {
1468
- const mgr = new ExtMgr();
1469
- await mgr.load();
1470
- if (email) await (mgr as any).remove?.(email);
1471
- else await (mgr as any).clear?.();
1472
- await mgr.save();
1473
- }
1474
- } catch {
1475
- const inline = new InlineAccountManager();
1476
- await inline.load();
1477
- if (!email) {
1478
- await saveAccountsInline([]);
1479
- } else {
1480
- const accs = await loadAccountsInline();
1481
- await saveAccountsInline(accs.filter((a) => a.email.toLowerCase() !== email.toLowerCase()));
1482
- }
1483
- }
1484
- },
1485
- /**
1486
- * Enable/disable toggle (merged from plugin(5)): disabled accounts are
1487
- * skipped by rotation until re-enabled.
1488
- */
1489
- async enable(email: string, en = true) {
1490
- await self.init();
1491
- try {
1492
- if (typeof (self.accountManager as any)?.enable === "function") {
1493
- await (self.accountManager as any).enable(email, en);
1494
- return;
1495
- }
1496
- } catch {
1497
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1498
- }
1499
- try {
1500
- const accs = await loadAccountsInline();
1501
- for (const a of accs) {
1502
- if (a.email.toLowerCase() === email.toLowerCase()) (a as any).disabled = !en;
1503
- }
1504
- await saveAccountsInline(accs);
1505
- } catch {
1506
- /* the guarded best-effort operation falls through: the outer flow owns the failure */
1507
- }
1508
- },
1509
- },
1510
- config: {
1511
- schema: {
1512
- oauth_identity: {
1513
- type: "string",
1514
- enum: ["auto", "antigravity-cli", "gemini-cli"],
1515
- default: "auto",
1516
- description:
1517
- "OAuth client identity for NEW logins: 'auto'/'antigravity-cli' masquerades as the Antigravity CLI (recommended — its client onboards the current free tier), 'gemini-cli' keeps the legacy Gemini CLI client. Stored accounts always refresh with the client they authenticated with.",
1518
- },
1519
- cli_first: {
1520
- type: "boolean",
1521
- default: false,
1522
- description:
1523
- "Route Gemini models to Gemini CLI quota first (preserve Antigravity quota for Claude) — dual quota",
1524
- },
1525
- pid_offset_enabled: {
1526
- type: "boolean",
1527
- default: false,
1528
- description: "Enable PID offset trick 0-1000 randomization for user_prompt_id (9router anti-fingerprint)",
1529
- },
1530
- google_search_enabled: {
1531
- type: ["boolean", "string"],
1532
- default: true,
1533
- description: "Enable google_search grounding; true/false force, 'auto' enables for Gemini models only",
1534
- },
1535
- quota_refresh_interval_minutes: { type: "number", default: 15 },
1536
- rotation_strategy: { type: "string", enum: ["round-robin", "sticky"], default: "round-robin" },
1537
- claude_tool_hardening: {
1538
- type: "boolean",
1539
- default: true,
1540
- description: "Prefix tool descriptions with hardening hints for Claude models",
1541
- },
1542
- keep_thinking: {
1543
- type: "boolean",
1544
- default: false,
1545
- description: "Keep thinking blocks/signatures in output instead of dropping reasoning parts",
1546
- },
1547
- soft_quota_threshold_percent: {
1548
- type: "number",
1549
- default: 90,
1550
- description: "Skip accounts at or above this used-percentage during rotation",
1551
- },
1552
- soft_quota_cache_ttl_minutes: {
1553
- type: ["number", "string"],
1554
- default: "auto",
1555
- description: "Soft-quota mark duration in minutes; 'auto' resolves to 15",
1556
- },
1557
- quiet_mode: { type: "boolean", default: false, description: "Suppress toast() notifications" },
1558
- },
1559
- defaults: {
1560
- cli_first: false,
1561
- pid_offset_enabled: false,
1562
- google_search_enabled: true,
1563
- quota_refresh_interval_minutes: 15,
1564
- rotation_strategy: "round-robin",
1565
- claude_tool_hardening: true,
1566
- keep_thinking: false,
1567
- soft_quota_threshold_percent: 90,
1568
- soft_quota_cache_ttl_minutes: "auto",
1569
- quiet_mode: false,
1570
- },
1571
- },
1572
- models: MODELS_2026_PROVIDER.map((id) => ({
1573
- id,
1574
- name: id,
1575
- provider: "google",
1576
- // v2.1.16 single-owner fix: context/output limits come from models.js
1577
- // (THE owner). gpt-oss intentionally corrected 131072/32768 ->
1578
- // 200000/64000 to match the models.ts catalog
1579
- // (CONTEXT_WINDOW_GPT_OSS / OUTPUT_LIMIT_GPT_OSS); claude/gemini values
1580
- // are byte-identical to the former inline numbers.
1581
- context: id.toLowerCase().includes("claude")
1582
- ? CONTEXT_WINDOW_CLAUDE
1583
- : id.toLowerCase().includes("gpt-oss")
1584
- ? CONTEXT_WINDOW_GPT_OSS
1585
- : CONTEXT_WINDOW_GEMINI,
1586
- output: id.toLowerCase().includes("claude")
1587
- ? OUTPUT_LIMIT_CLAUDE
1588
- : id.toLowerCase().includes("gpt-oss")
1589
- ? OUTPUT_LIMIT_GPT_OSS
1590
- : OUTPUT_LIMIT_GEMINI,
1591
- // api pool routing metadata (from ALL_MODELS_2026 merge)
1592
- api: mapModelToGroup(id),
1593
- modalities: { input: ["text", "image", "pdf"], output: ["text"] },
1594
- })),
1595
- };
1596
- }
1597
- }
1598
-
1599
- // singleton for opencode
1600
- let pluginInstance: AntigravityPlugin | null = null;
1601
-
1602
- export function getPlugin(options?: PluginOptions): AntigravityPlugin {
1603
- if (!pluginInstance) pluginInstance = new AntigravityPlugin(options);
1604
- return pluginInstance;
1605
- }
1606
-
1607
- /** Legacy definition export (pre-1.18 shape) kept for library consumers. */
1608
- export const plugin = getPlugin().getPluginDefinition();
1609
-
1610
- // =============================================================================
1611
- // OPENCODE PLUGIN MODULE BRIDGE (opencode >= 1.18 contract)
1612
- // =============================================================================
1613
- // opencode loads the plugin default export and requires the shape
1614
- // { id?: string, server: (input, options?) => Promise<Hooks> } - the loader
1615
- // rejects anything else with "must default export an object with server()".
1616
- // The bridge below adapts AntigravityPlugin to that contract:
1617
- //
1618
- // auth hook - provider "google" with one oauth method. authorize()
1619
- // starts the PKCE + local-callback flow (the very same
1620
- // Gemini CLI masquerade flow the CLI `login` command uses)
1621
- // and returns the Google consent URL; opencode renders it,
1622
- // and the returned callback() resolves with the exchanged
1623
- // tokens and persists the account in the robin-hood store.
1624
- // auth.loader - supplies the active account credentials without a login.
1625
- // chat.headers - injects the Gemini CLI identification (User-Agent,
1626
- // X-Goog-Api-Client, Client-Metadata) on every request:
1627
- // the dual-bypass second face.
1628
- // tool hooks - instrumentation passthrough kept from the legacy shape.
1629
- //
1630
- // The bridge reuses only node:* builtins plus the local modules - no new
1631
- // dependencies, no bundling requirements.
1632
-
1633
- /**
1634
- * Persists an OAuth account using the richest store available (the same
1635
- * write path as the legacy auth.login hook).
1636
- */
1637
- async function persistOAuthAccount(
1638
- email: string,
1639
- refresh: string,
1640
- access: string,
1641
- expiresIn: number,
1642
- identity?: {
1643
- identity: string;
1644
- clientId: string;
1645
- clientSecret: string;
1646
- scopes?: readonly string[];
1647
- },
1648
- ): Promise<void> {
1649
- const inst = getPlugin();
1650
- try {
1651
- await (inst as any).init?.();
1652
- } catch {
1653
- /* best effort */
1654
- }
1655
- try {
1656
- const m = (inst as any).accountManager;
1657
- if (m && typeof m.addAccount === "function") {
1658
- await m.addAccount({
1659
- email,
1660
- refreshToken: refresh,
1661
- accessToken: access,
1662
- expiryDate: Date.now() + expiresIn * 1000,
1663
- projectId: "", // left empty on purpose: the first request runs
1664
- // resolveProjectId (loadCodeAssist + onboardUser
1665
- // free-tier) and persists the account's REAL
1666
- // cloudaicompanionProject — the hardcoded fallback
1667
- // yields 403 PERMISSION_DENIED on generateContent.
1668
- enabled: true,
1669
- // identity the account authenticated with — refresh uses the same
1670
- // OAuth client (v3 store: oauthClientKey; inline store: type)
1671
- ...(identity
1672
- ? {
1673
- oauthClientKey: identity.identity,
1674
- oauthClientConfig: { clientId: identity.clientId, clientSecret: identity.clientSecret },
1675
- scopes: identity.scopes ? [...identity.scopes] : undefined,
1676
- type: identity.identity,
1677
- }
1678
- : {}),
1679
- });
1680
- return;
1681
- }
1682
- } catch {
1683
- /* fall through to the inline store */
1684
- }
1685
- const inline = new InlineAccountManager();
1686
- await inline.load();
1687
- await inline.addAccount({
1688
- email,
1689
- refreshToken: refresh,
1690
- accessToken: access,
1691
- ...(identity ? { type: identity.identity } : {}),
1692
- } as any);
1693
- }
1694
-
1695
- /**
1696
- * Detects which opencode.json provider block belongs to the provider plugin by looking at
1697
- * MODEL IDS ONLY (never the provider name): any provider whose models include
1698
- * `antigravity-*` prefixed ids or Google `gemini-*` ids is a provider consumer,
1699
- * whatever the user named it ("google", "antigravity", "casas-bahia", ...).
1700
- * Deterministic priority: antigravity-* models > gemini-* models; exact ids
1701
- * "google" / "antigravity" win ties; file order breaks remaining ties.
1702
- * Returns null when no config/provider qualifies (caller falls back "google").
1703
- */
1704
- export function detectOpencodeProviderId(configDir?: string): string | null {
1705
- const candidates: string[] = [];
1706
- // OPENCODE_CONFIG (opencode >= 1.18) points at the exact config FILE - the
1707
- // strongest signal there is, so it goes first (plus its dir's canonical
1708
- // names, in case the file itself is named differently).
1709
- const envFile = process.env.OPENCODE_CONFIG?.trim();
1710
- if (envFile) {
1711
- const absFile = resolve(envFile);
1712
- candidates.push(absFile, join(dirname(absFile), "opencode.json"), join(dirname(absFile), "config.json"));
1713
- }
1714
- const envDir = configDir ?? process.env.OPENCODE_CONFIG_DIR?.trim();
1715
- if (envDir) candidates.push(join(resolve(envDir), "opencode.json"), join(resolve(envDir), "config.json"));
1716
- candidates.push(
1717
- join(process.cwd(), "opencode.json"),
1718
- join(process.cwd(), ".opencode", "opencode.json"),
1719
- join(homedir(), ".config", "opencode", "opencode.json"),
1720
- join(homedir(), ".config", "opencode", "config.json"),
1721
- join(homedir(), ".opencode", "opencode.json"),
1722
- );
1723
- let cfg: any = null;
1724
- for (const p of candidates) {
1725
- try {
1726
- if (!existsSync(p)) continue;
1727
- const parsed = JSON.parse(readFileSync(p, "utf8"));
1728
- if (parsed && typeof parsed === "object") {
1729
- cfg = parsed;
1730
- break;
1731
- }
1732
- } catch {
1733
- /* unreadable candidate - try next */
1734
- }
1735
- }
1736
- const providers = cfg?.provider;
1737
- if (!providers || typeof providers !== "object") return null;
1738
- let best: { id: string; score: number } | null = null;
1739
- for (const [id, block] of Object.entries(providers as Record<string, any>)) {
1740
- if (!block || typeof block !== "object") continue;
1741
- const models = block.models;
1742
- let score = 0;
1743
- if (models && typeof models === "object") {
1744
- for (const modelId of Object.keys(models)) {
1745
- const l = modelId.toLowerCase();
1746
- if (l.startsWith("antigravity-")) {
1747
- score = Math.max(score, 100);
1748
- break;
1749
- }
1750
- if (l.startsWith("gemini-")) score = Math.max(score, 60);
1751
- }
1752
- }
1753
- const lid = id.toLowerCase();
1754
- if (score <= 0) {
1755
- // v2.1.20 — two STANDARD provider ids are recognized even without
1756
- // the provider model ids: "antigravity" (ours) and "google" (Google's).
1757
- // They score below any provider that actually carries
1758
- // antigravity-*/gemini-* models, so real model blocks always win.
1759
- if (lid === "antigravity") score = 50;
1760
- else if (lid === "google") score = 40;
1761
- else continue;
1762
- }
1763
- if (lid === "google") score += 10;
1764
- else if (lid === "antigravity") score += 5;
1765
- if (!best || score > best.score) best = { id, score };
1766
- }
1767
- return best ? best.id : null;
1768
- }
1769
-
1770
- // =============================================================================
1771
- // OPENCODE AUTH ENTRIES (opencode >= 1.18 contract)
1772
- // =============================================================================
1773
- // `opencode auth login` (and the TUI "Connect a provider" dialog) list one
1774
- // entry per plugin auth hook, keyed by auth.provider. the plugin registers THREE
1775
- // entries from a single plugin reference (see server.ts, the ./server
1776
- // package entry):
1777
- // - provider "google" — the standard Google entry
1778
- // - provider "antigravity" — the standard Antigravity entry
1779
- // - the CUSTOM provider detected by MODEL IDS in opencode.json
1780
- // ("casasbahia", "my-google", ... — whatever the user named it)
1781
- // Every entry shares the same credential loader (SDK options with the live
1782
- // OAuth token + identification headers + the cloudcode-pa fetch translator)
1783
- // and the same OAuth method (Antigravity CLI masquerade, PKCE, automatic
1784
- // loopback callback — no code copy/paste). The exchanged credential is
1785
- // stored under the provider that actually carries the provider models, so
1786
- // streaming works no matter which entry the user picked.
1787
-
1788
- /**
1789
- * Builds the shared credential loader: opencode merges the return straight
1790
- * into the provider SDK options (baseURL + apiKey = the live OAuth access
1791
- * token + the identity identification headers + a fetch override wired to
1792
- * the plugin handleFetch translator, which rewrites every chat request into
1793
- * the Antigravity/Gemini-CLI cloudcode-pa envelope).
1794
- */
1795
- function buildOpencodeAuthLoader(inst: AntigravityPlugin, legacyAuth: any) {
1796
- return async (): Promise<Record<string, any> | null> => {
1797
- try {
1798
- const legacyInfo = (await legacyAuth.loader?.()) as any;
1799
- if (!legacyInfo?.accessToken) return null;
1800
- // request fingerprint of the active account's identity — the
1801
- // antigravity-cli client presents its own UA/api-client pair, the
1802
- // gemini-cli family keeps the GeminiCLI fingerprint
1803
- const activeAccount = (() => {
1804
- try {
1805
- const list = (inst as any).accountManager?.list?.() ?? (inst as any).accountManager?.accounts ?? [];
1806
- return Array.isArray(list) ? (list.find((a: any) => a?.email === legacyInfo.email) ?? list[0] ?? null) : null;
1807
- } catch {
1808
- return null;
1809
- }
1810
- })();
1811
- const isAgCli =
1812
- identityFromAccountType((activeAccount as any)?.type ?? (activeAccount as any)?.oauthClientKey) ===
1813
- "antigravity-cli";
1814
- const headers = isAgCli
1815
- ? getAntigravityCliHeaders()
1816
- : ((oauthMod as any).getGeminiHeaders?.() ?? FALLBACK_GEMINI_HEADERS);
1817
- return {
1818
- // v2.1.16 single-owner fix: cloudcode-pa base URL from constants.js
1819
- // (CLOUDCODE_BASE_URL owner) instead of a local literal.
1820
- baseURL: CLOUDCODE_BASE_URL,
1821
- apiKey: legacyInfo.accessToken,
1822
- headers,
1823
- fetch: (input: any, init?: any) => (inst as any).handleFetch(fetch, input, init),
1824
- };
1825
- } catch {
1826
- return null;
1827
- }
1828
- };
1829
- }
1830
-
1831
- /**
1832
- * Resolves the provider id the OAuth credential is stored under: the
1833
- * detected provider (model ids first) — otherwise the entry's own provider,
1834
- * except that "antigravity" is only kept when the provider will actually
1835
- * exist (user config or the zero-config injection); with the injection
1836
- * disabled and nothing configured the credential falls back to "google",
1837
- * which always exists in opencode's models.dev database (a credential whose
1838
- * provider exists nowhere crashes opencode's auth-loader merge).
1839
- */
1840
- function resolveCredentialTarget(fallbackProvider: string): string {
1841
- const detected = detectOpencodeProviderId();
1842
- if (detected) return detected;
1843
- if (fallbackProvider === "antigravity") {
1844
- const injectionEnabled =
1845
- ((getPlugin() as any)?.options?.inject_antigravity_provider as boolean | undefined) !== false;
1846
- return injectionEnabled ? "antigravity" : "google";
1847
- }
1848
- return fallbackProvider;
1849
- }
1850
-
1851
- /**
1852
- * Builds the shared OAuth method list: authorize() starts the PKCE +
1853
- * local-callback flow (the Antigravity CLI masquerade — the very same
1854
- * identity the CLI `login` command uses) and returns the Google consent
1855
- * URL; the returned callback() resolves with the exchanged tokens, persists
1856
- * the account in the robin-hood store and reports the provider the
1857
- * credential is stored under (model-id detection first, the entry's own
1858
- * provider id as fallback — so a credential picked on the "google" entry
1859
- * still lands on the custom provider that carries the provider models).
1860
- */
1861
- function buildOpencodeAuthMethods(fallbackProvider: string) {
1862
- return [
1863
- {
1864
- type: "oauth" as const,
1865
- label: "Login with Google (Antigravity - Gemini CLI bypass)",
1866
-
1867
- /**
1868
- * Starts the OAuth flow and returns the Google consent URL plus
1869
- * the callback that resolves once the tokens are exchanged.
1870
- * opencode renders the URL; after the user signs in and grants
1871
- * the consent, Google redirects to the local 127.0.0.1 callback
1872
- * server started here.
1873
- */
1874
- authorize: async (): Promise<{
1875
- url: string;
1876
- instructions: string;
1877
- method: "auto";
1878
- callback: () => Promise<
1879
- | {
1880
- type: "success";
1881
- provider?: string | undefined;
1882
- refresh: string;
1883
- access: string;
1884
- expires: number;
1885
- accountId?: string | undefined;
1886
- }
1887
- | { type: "failed" }
1888
- >;
1889
- }> => {
1890
- // OAuth identity for NEW logins: the Antigravity CLI client
1891
- // (default since 2.1.8 — the gemini-cli individuals tier was
1892
- // retired; the antigravity-cli client onboards the "Antigravity"
1893
- // free tier and its loopback redirect is accepted by Google,
1894
- // which keeps the whole flow automatic: no code copy/paste).
1895
- const inst = getPlugin();
1896
- const identity = resolveOAuthIdentity(((inst as any)?.options?.oauth_identity as string) ?? "auto");
1897
- const state = randomBytes(32).toString("base64url");
1898
- const pkce = identity.usesPKCE ? ((oauthMod as any).generatePKCE?.() ?? null) : null;
1899
- const srv = await (oauthMod as any).createOAuthServerAsync({
1900
- callbackPath: "/oauth2callback",
1901
- expectedState: state,
1902
- });
1903
- const redirectUri: string = srv.callbackUrl;
1904
- const authUrl: string = (oauthMod as any).buildAuthUrl(pkce, redirectUri, {
1905
- state,
1906
- accessType: "offline",
1907
- scopes: identity.scopesStr,
1908
- clientId: identity.clientId,
1909
- ...(identity.usesPKCE ? { prompt: "consent" as const } : {}),
1910
- });
1911
-
1912
- return {
1913
- url: authUrl,
1914
- instructions: `Sign in with your Google account and authorize ${identity.appLabel}. the plugin masquerades as the ${identity.identity} client and stores the tokens locally for the robin-hood rotation.`,
1915
- method: "auto",
1916
- callback: async () => {
1917
- try {
1918
- const { code } = await srv.waitForCallback();
1919
- const tokens = await (oauthMod as any).exchangeCodeForTokens(
1920
- code,
1921
- pkce ? pkce.verifier : null,
1922
- redirectUri,
1923
- { clientId: identity.clientId, clientSecret: identity.clientSecret },
1924
- );
1925
- const refresh: string | undefined = tokens.refresh_token;
1926
- const access: string | undefined = tokens.access_token;
1927
- const expires: number = Date.now() + (tokens.expires_in ?? 3600) * 1000;
1928
- if (!refresh || !access) {
1929
- return { type: "failed" as const };
1930
- }
1931
- let email = "";
1932
- try {
1933
- const userInfo = await (oauthMod as any).getUserInfo(access);
1934
- email = (userInfo?.email ?? "").toLowerCase();
1935
- } catch {
1936
- /* userinfo is optional for the credential */
1937
- }
1938
- await persistOAuthAccount(
1939
- email || `user-${Date.now()}@gmail.com`,
1940
- refresh,
1941
- access,
1942
- tokens.expires_in ?? 3600,
1943
- identity,
1944
- );
1945
- return {
1946
- type: "success" as const,
1947
- provider: resolveCredentialTarget(fallbackProvider),
1948
- refresh,
1949
- access,
1950
- expires,
1951
- accountId: email || undefined,
1952
- };
1953
- } catch {
1954
- return { type: "failed" as const };
1955
- } finally {
1956
- try {
1957
- await srv.close();
1958
- } catch {
1959
- /* server already down */
1960
- }
1961
- }
1962
- },
1963
- };
1964
- },
1965
- },
1966
- ];
1967
- }
1968
-
1969
- /**
1970
- * Builds a complete opencode AuthHook for one provider id (shared loader +
1971
- * the OAuth methods). Used by the ./server entry for the standard "google"
1972
- * and "antigravity" entries and for the custom provider detected by model
1973
- * ids — all three surface in `opencode auth login` with an OAuth option.
1974
- */
1975
- export function buildOpencodeAuthHook(providerId: string): Record<string, any> {
1976
- const inst = getPlugin();
1977
- const def = inst.getPluginDefinition();
1978
- const legacyAuth: any = def.auth ?? {};
1979
- return {
1980
- // Recognition is by MODEL IDS in opencode.json, never by a fixed name:
1981
- // the user may name the provider anything ("google", "casas-bahia", ...)
1982
- // as long as it carries antigravity-* / gemini-* model ids. The
1983
- // standard entries "google" and "antigravity" work with zero config.
1984
- provider: providerId,
1985
- loader: buildOpencodeAuthLoader(inst, legacyAuth),
1986
- methods: buildOpencodeAuthMethods(providerId),
1987
- };
1988
- }
1989
-
1990
- /**
1991
- * Standard-entry safety net: ensures a provider "antigravity" ALWAYS exists
1992
- * in the config (npm @ai-sdk/google + the provider model catalog) unless the
1993
- * user defined their own "antigravity" block or disabled the provider.
1994
- * This is one of the TWO standard entries ("google" is the other — it always
1995
- * exists in opencode's models.dev database): the "Antigravity" entry of
1996
- * `opencode auth login` is immediately usable, and credentials stored under
1997
- * "antigravity" (e.g. from a zero-config login on another project) always
1998
- * find their provider — opencode crashes on auth-loader credentials whose
1999
- * provider exists neither in models.dev nor in the config. ADD-ONLY and
2000
- * in-memory: the user's file is never touched and an explicit
2001
- * "antigravity" block always wins.
2002
- */
2003
- export function injectAntigravityProvider(cfg: any, enabled = true): void {
2004
- try {
2005
- if (!enabled || !cfg || typeof cfg !== "object") return;
2006
- let providers = cfg.provider;
2007
- if (!providers || typeof providers !== "object" || Array.isArray(providers)) {
2008
- providers = {};
2009
- cfg.provider = providers;
2010
- }
2011
- if (providers.antigravity) return; // the user already defined it — never override
2012
- const disabled = Array.isArray(cfg.disabled_providers) ? cfg.disabled_providers.map(String) : [];
2013
- if (disabled.includes("antigravity")) return; // explicitly disabled
2014
- const models: Record<string, any> = {};
2015
- for (const m of ALL_MODELS_2026) models[m.id] = {};
2016
- providers.antigravity = {
2017
- npm: "@ai-sdk/google",
2018
- name: "Antigravity",
2019
- options: {},
2020
- models,
2021
- };
2022
- } catch {
2023
- /* never break the host config */
2024
- }
2025
- }
2026
-
2027
- /**
2028
- * The opencode server entry: initializes the plugin singleton and returns
2029
- * the hooks object implementing the 1.18.x contract — header injection,
2030
- * the zero-config antigravity provider injection and the auth entry for
2031
- * the CUSTOM provider detected by model ids. The standard "google" and
2032
- * "antigravity" auth entries are registered by server.ts (the ./server
2033
- * package entry) and are never duplicated here.
2034
- */
2035
- export async function opencodeServer(_input?: unknown, options?: PluginOptions): Promise<Record<string, any>> {
2036
- const inst = getPlugin(options ?? {});
2037
- try {
2038
- await (inst as any).init?.();
2039
- } catch {
2040
- /* plugin init is best-effort; the hooks degrade gracefully */
2041
- }
2042
- const def = inst.getPluginDefinition();
2043
- const legacyAuth: any = def.auth ?? {};
2044
- const legacyHooks: any = def.hooks ?? {};
2045
-
2046
- // v2.1.13 — provider scoping for the identification spoof: only requests
2047
- // whose PROVIDER is a provider-family entry (the detected custom provider plus the
2048
- // standard "google" / "antigravity" ids) may receive the Google
2049
- // identification headers. Model-id substrings alone are NOT enough: real
2050
- // configs carry e.g. "openai/gpt-oss-120b" under nvidia/openrouter, and
2051
- // those third-party requests must never be polluted with the spoof.
2052
- const devthinkProviderIds = new Set(
2053
- ["google", "antigravity", detectOpencodeProviderId()]
2054
- .filter((x): x is string => typeof x === "string" && x.length > 0)
2055
- .map((x) => x.toLowerCase()),
2056
- );
2057
-
2058
- const hooks: Record<string, any> = {
2059
- // keep the valid legacy hooks (tool instrumentation); the legacy "fetch"
2060
- // hook is not part of the 1.18 contract and is intentionally dropped
2061
- // here - the identification spoof moves to chat.headers below.
2062
- ...(legacyHooks["tool.execute.before"] ? { "tool.execute.before": legacyHooks["tool.execute.before"] } : {}),
2063
- ...(legacyHooks["tool.execute.after"] ? { "tool.execute.after": legacyHooks["tool.execute.after"] } : {}),
2064
-
2065
- /**
2066
- * Gemini CLI identification spoof on chat requests for provider-handled
2067
- * models: gated to MAENE PROVIDER IDS (detected custom provider + the
2068
- * standard "google"/"antigravity" entries — see devthinkProviderIds above)
2069
- * AND a matching MODEL ID (antigravity-*, gemini-*, gpt-oss): User-Agent
2070
- * GeminiCLI/0.57.0, X-Goog-Api-Client gl-node/..., Client-Metadata
2071
- * ideType=IDE_UNSPECIFIED. Unrelated providers (nvidia/openrouter/...)
2072
- * never receive the spoof even when their model ids contain the
2073
- * substrings ("openai/gpt-oss-120b" etc.).
2074
- */
2075
- "chat.headers": async (
2076
- _input: {
2077
- sessionID: string;
2078
- agent: string;
2079
- model?: { providerID: string; modelID: string };
2080
- provider?: { info?: { options?: Record<string, any> } };
2081
- },
2082
- output: { headers: Record<string, string> },
2083
- ): Promise<void> => {
2084
- try {
2085
- const modelId = String(_input?.model?.modelID ?? "").toLowerCase();
2086
- if (!modelId) return;
2087
- const providerId = String(_input?.model?.providerID ?? "").toLowerCase();
2088
- if (!devthinkProviderIds.has(providerId)) return; // never pollute unrelated providers with the spoof
2089
- const handled = modelId.includes("antigravity") || modelId.includes("gemini") || modelId.includes("gpt-oss");
2090
- if (!handled) return;
2091
- const spoof = (oauthMod as any).getGeminiHeaders?.() ?? FALLBACK_GEMINI_HEADERS;
2092
- Object.assign(output.headers, spoof);
2093
- } catch {
2094
- /* header injection never breaks the request */
2095
- }
2096
- },
2097
-
2098
- /**
2099
- * config hook (opencode >= 1.18): runs before opencode reads
2100
- * cfg.provider — the documented injection point. Used only for the
2101
- * zero-config antigravity provider; the user's file is never touched
2102
- * on disk and any real provider block (custom name included) takes
2103
- * precedence. Disable with the plugin option
2104
- * ["devthink", { "inject_antigravity_provider": false }].
2105
- */
2106
- config: async (input: any): Promise<void> => {
2107
- injectAntigravityProvider(input, (options as any)?.inject_antigravity_provider !== false);
2108
- },
2109
- };
2110
-
2111
- // Auth for the CUSTOM provider detected by MODEL IDS in opencode.json
2112
- // (any name the user chose). The standard "google" and "antigravity"
2113
- // entries are registered by server.ts (the ./server package entry) —
2114
- // never duplicated here.
2115
- const detected = detectOpencodeProviderId();
2116
- const customCandidate =
2117
- detected ?? (typeof legacyAuth.provider === "string" && legacyAuth.provider ? legacyAuth.provider : null);
2118
- if (customCandidate && customCandidate !== "google" && customCandidate !== "antigravity") {
2119
- hooks.auth = buildOpencodeAuthHook(customCandidate);
2120
- }
2121
- return hooks;
2122
- }
2123
-
2124
- /**
2125
- * The opencode PluginModule (V1 shape): what loaders that read package.json
2126
- * `main` directly get (library consumers, older opencode builds). It always
2127
- * carries exactly one auth entry — the detected provider with the "google"
2128
- * fallback — so single-hook loaders keep working.
2129
- */
2130
- const opencodePluginModule = {
2131
- id: "devthink",
2132
- server: async (input?: unknown, options?: PluginOptions) => {
2133
- const hooks = await opencodeServer(input, options);
2134
- if (!hooks.auth) hooks.auth = buildOpencodeAuthHook(detectOpencodeProviderId() ?? "google");
2135
- return hooks;
2136
- },
2137
- } as const;
2138
-
2139
- export default opencodePluginModule;