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