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