@agentrhq/webcmd 0.1.1 → 0.1.2
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/README.md +317 -24
- package/dist/src/adapter-shadow.d.ts +11 -0
- package/dist/src/adapter-shadow.js +72 -0
- package/dist/src/adapter-shadow.test.d.ts +1 -0
- package/dist/src/adapter-shadow.test.js +49 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.d.ts +1 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/brand.d.ts +9 -0
- package/dist/src/brand.js +9 -0
- package/dist/src/brand.test.d.ts +1 -0
- package/dist/src/brand.test.js +15 -0
- package/dist/src/browser/analyze.d.ts +118 -0
- package/dist/src/browser/analyze.js +405 -0
- package/dist/src/browser/analyze.test.d.ts +1 -0
- package/dist/src/browser/analyze.test.js +209 -0
- package/dist/src/browser/article-extract.d.ts +57 -0
- package/dist/src/browser/article-extract.e2e.test.d.ts +1 -0
- package/dist/src/browser/article-extract.e2e.test.js +105 -0
- package/dist/src/browser/article-extract.js +169 -0
- package/dist/src/browser/article-extract.test.d.ts +1 -0
- package/dist/src/browser/article-extract.test.js +94 -0
- package/dist/src/browser/ax-snapshot.d.ts +37 -0
- package/dist/src/browser/ax-snapshot.js +217 -0
- package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
- package/dist/src/browser/ax-snapshot.test.js +91 -0
- package/dist/src/browser/base-page.d.ts +151 -0
- package/dist/src/browser/base-page.js +1060 -0
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +803 -0
- package/dist/src/browser/bridge-readiness.d.ts +14 -0
- package/dist/src/browser/bridge-readiness.js +24 -0
- package/dist/src/browser/bridge-readiness.test.d.ts +1 -0
- package/dist/src/browser/bridge-readiness.test.js +58 -0
- package/dist/src/browser/bridge.d.ts +26 -0
- package/dist/src/browser/bridge.js +58 -0
- package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
- package/dist/src/browser/cdp-click-fixture.test.js +87 -0
- package/dist/src/browser/cdp.d.ts +46 -0
- package/dist/src/browser/cdp.js +536 -0
- package/dist/src/browser/cdp.test.d.ts +1 -0
- package/dist/src/browser/cdp.test.js +79 -0
- package/dist/src/browser/compound.d.ts +59 -0
- package/dist/src/browser/compound.js +112 -0
- package/dist/src/browser/compound.test.d.ts +1 -0
- package/dist/src/browser/compound.test.js +175 -0
- package/dist/src/browser/config.d.ts +2 -0
- package/dist/src/browser/config.js +14 -0
- package/dist/src/browser/daemon-client.d.ts +32 -0
- package/dist/src/browser/daemon-client.js +135 -0
- package/dist/src/browser/daemon-client.test.d.ts +1 -0
- package/dist/src/browser/daemon-client.test.js +326 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +37 -0
- package/dist/src/browser/daemon-lifecycle.js +147 -0
- package/dist/src/browser/daemon-transport.d.ts +54 -0
- package/dist/src/browser/daemon-transport.js +68 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -0
- package/dist/src/browser/dom-helpers.d.ts +41 -0
- package/dist/src/browser/dom-helpers.js +239 -0
- package/dist/src/browser/dom-helpers.test.d.ts +1 -0
- package/dist/src/browser/dom-helpers.test.js +106 -0
- package/dist/src/browser/dom-snapshot.d.ts +93 -0
- package/dist/src/browser/dom-snapshot.js +894 -0
- package/dist/src/browser/dom-snapshot.test.d.ts +11 -0
- package/dist/src/browser/dom-snapshot.test.js +313 -0
- package/dist/src/browser/errors.d.ts +38 -0
- package/dist/src/browser/errors.js +79 -0
- package/dist/src/browser/errors.test.d.ts +1 -0
- package/dist/src/browser/errors.test.js +60 -0
- package/dist/src/browser/extract.d.ts +69 -0
- package/dist/src/browser/extract.js +132 -0
- package/dist/src/browser/extract.test.d.ts +1 -0
- package/dist/src/browser/extract.test.js +129 -0
- package/dist/src/browser/find.d.ts +84 -0
- package/dist/src/browser/find.js +398 -0
- package/dist/src/browser/find.test.d.ts +1 -0
- package/dist/src/browser/find.test.js +180 -0
- package/dist/src/browser/html-tree.d.ts +75 -0
- package/dist/src/browser/html-tree.js +112 -0
- package/dist/src/browser/html-tree.test.d.ts +1 -0
- package/dist/src/browser/html-tree.test.js +181 -0
- package/dist/src/browser/index.d.ts +14 -0
- package/dist/src/browser/index.js +12 -0
- package/dist/src/browser/network-cache.d.ts +49 -0
- package/dist/src/browser/network-cache.js +78 -0
- package/dist/src/browser/network-cache.test.d.ts +1 -0
- package/dist/src/browser/network-cache.test.js +75 -0
- package/dist/src/browser/network-interceptor.d.ts +11 -0
- package/dist/src/browser/network-interceptor.js +11 -0
- package/dist/src/browser/network-key.d.ts +22 -0
- package/dist/src/browser/network-key.js +66 -0
- package/dist/src/browser/network-key.test.d.ts +1 -0
- package/dist/src/browser/network-key.test.js +49 -0
- package/dist/src/browser/page.d.ts +83 -0
- package/dist/src/browser/page.js +453 -0
- package/dist/src/browser/page.test.d.ts +1 -0
- package/dist/src/browser/page.test.js +406 -0
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +86 -0
- package/dist/src/browser/protocol.d.ts +63 -0
- package/dist/src/browser/protocol.js +1 -0
- package/dist/src/browser/runtime/local-cloak/actions.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +240 -0
- package/dist/src/browser/runtime/local-cloak/downloads.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/downloads.js +28 -0
- package/dist/src/browser/runtime/local-cloak/network.d.ts +28 -0
- package/dist/src/browser/runtime/local-cloak/network.js +120 -0
- package/dist/src/browser/runtime/local-cloak/network.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/network.test.js +124 -0
- package/dist/src/browser/runtime/local-cloak/profiles.d.ts +6 -0
- package/dist/src/browser/runtime/local-cloak/profiles.js +17 -0
- package/dist/src/browser/runtime/local-cloak/profiles.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/profiles.test.js +19 -0
- package/dist/src/browser/runtime/local-cloak/provider.d.ts +15 -0
- package/dist/src/browser/runtime/local-cloak/provider.js +27 -0
- package/dist/src/browser/runtime/local-cloak/provider.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/provider.test.js +244 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +77 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +280 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +92 -0
- package/dist/src/browser/runtime/provider.d.ts +9 -0
- package/dist/src/browser/runtime/provider.js +1 -0
- package/dist/src/browser/runtime/provider.test.d.ts +2 -0
- package/dist/src/browser/runtime/provider.test.js +49 -0
- package/dist/src/browser/shape-filter.d.ts +52 -0
- package/dist/src/browser/shape-filter.js +101 -0
- package/dist/src/browser/shape-filter.test.d.ts +1 -0
- package/dist/src/browser/shape-filter.test.js +101 -0
- package/dist/src/browser/shape.d.ts +23 -0
- package/dist/src/browser/shape.js +95 -0
- package/dist/src/browser/shape.test.d.ts +1 -0
- package/dist/src/browser/shape.test.js +82 -0
- package/dist/src/browser/stealth.d.ts +11 -0
- package/dist/src/browser/stealth.js +359 -0
- package/dist/src/browser/stealth.test.d.ts +1 -0
- package/dist/src/browser/stealth.test.js +134 -0
- package/dist/src/browser/tabs.d.ts +13 -0
- package/dist/src/browser/tabs.js +70 -0
- package/dist/src/browser/target-errors.d.ts +39 -0
- package/dist/src/browser/target-errors.js +45 -0
- package/dist/src/browser/target-errors.test.d.ts +1 -0
- package/dist/src/browser/target-errors.test.js +94 -0
- package/dist/src/browser/target-resolver.d.ts +158 -0
- package/dist/src/browser/target-resolver.js +570 -0
- package/dist/src/browser/target-resolver.test.d.ts +1 -0
- package/dist/src/browser/target-resolver.test.js +118 -0
- package/dist/src/browser/utils.d.ts +20 -0
- package/dist/src/browser/utils.js +64 -0
- package/dist/src/browser/utils.test.d.ts +1 -0
- package/dist/src/browser/utils.test.js +29 -0
- package/dist/src/browser/verify-fixture.d.ts +65 -0
- package/dist/src/browser/verify-fixture.js +318 -0
- package/dist/src/browser/verify-fixture.test.d.ts +1 -0
- package/dist/src/browser/verify-fixture.test.js +219 -0
- package/dist/src/browser/visual-refs.d.ts +11 -0
- package/dist/src/browser/visual-refs.js +108 -0
- package/dist/src/browser.test.d.ts +1 -0
- package/dist/src/browser.test.js +297 -0
- package/dist/src/build-manifest.d.ts +103 -0
- package/dist/src/build-manifest.js +363 -0
- package/dist/src/build-manifest.test.d.ts +1 -0
- package/dist/src/build-manifest.test.js +341 -0
- package/dist/src/capabilityRouting.d.ts +19 -0
- package/dist/src/capabilityRouting.js +56 -0
- package/dist/src/capabilityRouting.test.d.ts +1 -0
- package/dist/src/capabilityRouting.test.js +63 -0
- package/dist/src/cli-argv-preprocess.d.ts +63 -0
- package/dist/src/cli-argv-preprocess.js +303 -0
- package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
- package/dist/src/cli-argv-preprocess.test.js +304 -0
- package/dist/src/cli.d.ts +79 -0
- package/dist/src/cli.js +3350 -0
- package/dist/src/cli.test.d.ts +1 -0
- package/dist/src/cli.test.js +3041 -0
- package/dist/src/commanderAdapter.d.ts +21 -0
- package/dist/src/commanderAdapter.js +208 -0
- package/dist/src/commanderAdapter.test.d.ts +1 -0
- package/dist/src/commanderAdapter.test.js +352 -0
- package/dist/src/commands/auth.d.ts +40 -0
- package/dist/src/commands/auth.js +434 -0
- package/dist/src/commands/auth.test.d.ts +1 -0
- package/dist/src/commands/auth.test.js +252 -0
- package/dist/src/commands/daemon.d.ts +9 -0
- package/dist/src/commands/daemon.js +91 -0
- package/dist/src/commands/daemon.test.d.ts +1 -0
- package/dist/src/commands/daemon.test.js +293 -0
- package/dist/src/completion-fast.d.ts +22 -0
- package/dist/src/completion-fast.js +88 -0
- package/dist/src/completion-shared.d.ts +13 -0
- package/dist/src/completion-shared.js +61 -0
- package/dist/src/completion.d.ts +21 -0
- package/dist/src/completion.js +68 -0
- package/dist/src/completion.test.d.ts +1 -0
- package/dist/src/completion.test.js +24 -0
- package/dist/src/constants.d.ts +18 -0
- package/dist/src/constants.js +41 -0
- package/dist/src/convention-audit.d.ts +50 -0
- package/dist/src/convention-audit.js +553 -0
- package/dist/src/convention-audit.test.d.ts +1 -0
- package/dist/src/convention-audit.test.js +255 -0
- package/dist/src/daemon/server.d.ts +13 -0
- package/dist/src/daemon/server.js +185 -0
- package/dist/src/daemon/server.test.d.ts +1 -0
- package/dist/src/daemon/server.test.js +132 -0
- package/dist/src/daemon-utils.d.ts +18 -0
- package/dist/src/daemon-utils.js +37 -0
- package/dist/src/daemon.d.ts +1 -0
- package/dist/src/daemon.js +27 -0
- package/dist/src/daemon.test.d.ts +1 -0
- package/dist/src/daemon.test.js +60 -0
- package/dist/src/discovery.d.ts +46 -0
- package/dist/src/discovery.js +341 -0
- package/dist/src/doctor.d.ts +39 -0
- package/dist/src/doctor.js +168 -0
- package/dist/src/doctor.test.d.ts +1 -0
- package/dist/src/doctor.test.js +302 -0
- package/dist/src/download/article-download.d.ts +72 -0
- package/dist/src/download/article-download.js +305 -0
- package/dist/src/download/article-download.test.d.ts +1 -0
- package/dist/src/download/article-download.test.js +235 -0
- package/dist/src/download/index.d.ts +71 -0
- package/dist/src/download/index.js +363 -0
- package/dist/src/download/index.test.d.ts +1 -0
- package/dist/src/download/index.test.js +174 -0
- package/dist/src/download/media-download.d.ts +49 -0
- package/dist/src/download/media-download.js +127 -0
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.d.ts +36 -0
- package/dist/src/download/progress.js +120 -0
- package/dist/src/download/progress.test.d.ts +1 -0
- package/dist/src/download/progress.test.js +36 -0
- package/dist/src/electron-apps.d.ts +31 -0
- package/dist/src/electron-apps.js +91 -0
- package/dist/src/electron-apps.test.d.ts +1 -0
- package/dist/src/electron-apps.test.js +76 -0
- package/dist/src/engine.test.d.ts +1 -0
- package/dist/src/engine.test.js +312 -0
- package/dist/src/errors.d.ts +114 -0
- package/dist/src/errors.js +174 -0
- package/dist/src/errors.test.d.ts +1 -0
- package/dist/src/errors.test.js +109 -0
- package/dist/src/execution.d.ts +24 -0
- package/dist/src/execution.js +509 -0
- package/dist/src/execution.test.d.ts +1 -0
- package/dist/src/execution.test.js +645 -0
- package/dist/src/external.d.ts +49 -0
- package/dist/src/external.js +211 -0
- package/dist/src/external.test.d.ts +1 -0
- package/dist/src/external.test.js +110 -0
- package/dist/src/help.d.ts +88 -0
- package/dist/src/help.js +556 -0
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +58 -0
- package/dist/src/hooks.d.ts +46 -0
- package/dist/src/hooks.js +58 -0
- package/dist/src/hooks.test.d.ts +4 -0
- package/dist/src/hooks.test.js +92 -0
- package/dist/src/interceptor.d.ts +44 -0
- package/dist/src/interceptor.js +183 -0
- package/dist/src/interceptor.test.d.ts +4 -0
- package/dist/src/interceptor.test.js +81 -0
- package/dist/src/launcher.d.ts +41 -0
- package/dist/src/launcher.js +226 -0
- package/dist/src/launcher.test.d.ts +1 -0
- package/dist/src/launcher.test.js +153 -0
- package/dist/src/logger.d.ts +26 -0
- package/dist/src/logger.js +49 -0
- package/dist/src/main.d.ts +5 -0
- package/dist/src/main.js +177 -0
- package/dist/src/manifest-types.d.ts +43 -0
- package/dist/src/manifest-types.js +9 -0
- package/dist/src/node-network.d.ts +10 -0
- package/dist/src/node-network.js +174 -0
- package/dist/src/node-network.test.d.ts +1 -0
- package/dist/src/node-network.test.js +55 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/output.d.ts +14 -0
- package/dist/src/output.js +152 -0
- package/dist/src/output.test.d.ts +1 -0
- package/dist/src/output.test.js +53 -0
- package/dist/src/package-exports.test.d.ts +1 -0
- package/dist/src/package-exports.test.js +112 -0
- package/dist/src/package-paths.d.ts +8 -0
- package/dist/src/package-paths.js +41 -0
- package/dist/src/pipeline/executor.d.ts +11 -0
- package/dist/src/pipeline/executor.js +93 -0
- package/dist/src/pipeline/executor.test.d.ts +4 -0
- package/dist/src/pipeline/executor.test.js +182 -0
- package/dist/src/pipeline/index.d.ts +5 -0
- package/dist/src/pipeline/index.js +5 -0
- package/dist/src/pipeline/registry.d.ts +28 -0
- package/dist/src/pipeline/registry.js +55 -0
- package/dist/src/pipeline/steps/browser.d.ts +13 -0
- package/dist/src/pipeline/steps/browser.js +86 -0
- package/dist/src/pipeline/steps/download.d.ts +18 -0
- package/dist/src/pipeline/steps/download.js +252 -0
- package/dist/src/pipeline/steps/download.test.d.ts +1 -0
- package/dist/src/pipeline/steps/download.test.js +102 -0
- package/dist/src/pipeline/steps/fetch.d.ts +5 -0
- package/dist/src/pipeline/steps/fetch.js +122 -0
- package/dist/src/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/src/pipeline/steps/fetch.test.js +117 -0
- package/dist/src/pipeline/steps/intercept.d.ts +5 -0
- package/dist/src/pipeline/steps/intercept.js +50 -0
- package/dist/src/pipeline/steps/tap.d.ts +12 -0
- package/dist/src/pipeline/steps/tap.js +93 -0
- package/dist/src/pipeline/steps/transform.d.ts +9 -0
- package/dist/src/pipeline/steps/transform.js +70 -0
- package/dist/src/pipeline/template.d.ts +17 -0
- package/dist/src/pipeline/template.js +344 -0
- package/dist/src/pipeline/template.test.d.ts +4 -0
- package/dist/src/pipeline/template.test.js +197 -0
- package/dist/src/pipeline/transform.test.d.ts +4 -0
- package/dist/src/pipeline/transform.test.js +133 -0
- package/dist/src/plugin-manifest.d.ts +70 -0
- package/dist/src/plugin-manifest.js +160 -0
- package/dist/src/plugin-manifest.test.d.ts +4 -0
- package/dist/src/plugin-manifest.test.js +179 -0
- package/dist/src/plugin-scaffold.d.ts +28 -0
- package/dist/src/plugin-scaffold.js +143 -0
- package/dist/src/plugin-scaffold.test.d.ts +4 -0
- package/dist/src/plugin-scaffold.test.js +83 -0
- package/dist/src/plugin.d.ts +146 -0
- package/dist/src/plugin.js +1303 -0
- package/dist/src/plugin.test.d.ts +4 -0
- package/dist/src/plugin.test.js +1363 -0
- package/dist/src/registry-api.d.ts +13 -0
- package/dist/src/registry-api.js +10 -0
- package/dist/src/registry.d.ts +123 -0
- package/dist/src/registry.js +125 -0
- package/dist/src/registry.test.d.ts +4 -0
- package/dist/src/registry.test.js +209 -0
- package/dist/src/runtime-copy.test.d.ts +1 -0
- package/dist/src/runtime-copy.test.js +29 -0
- package/dist/src/runtime-detect.d.ts +31 -0
- package/dist/src/runtime-detect.js +47 -0
- package/dist/src/runtime-detect.test.d.ts +1 -0
- package/dist/src/runtime-detect.test.js +39 -0
- package/dist/src/runtime-identity.test.d.ts +1 -0
- package/dist/src/runtime-identity.test.js +18 -0
- package/dist/src/runtime.d.ts +47 -0
- package/dist/src/runtime.js +54 -0
- package/dist/src/scripts/framework.d.ts +7 -0
- package/dist/src/scripts/framework.js +25 -0
- package/dist/src/scripts/interact.d.ts +4 -0
- package/dist/src/scripts/interact.js +20 -0
- package/dist/src/scripts/store.d.ts +13 -0
- package/dist/src/scripts/store.js +48 -0
- package/dist/src/serialization.d.ts +42 -0
- package/dist/src/serialization.js +103 -0
- package/dist/src/serialization.test.d.ts +1 -0
- package/dist/src/serialization.test.js +78 -0
- package/dist/src/skills.d.ts +14 -0
- package/dist/src/skills.js +127 -0
- package/dist/src/skills.test.d.ts +1 -0
- package/dist/src/skills.test.js +71 -0
- package/dist/src/snapshotFormatter.d.ts +11 -0
- package/dist/src/snapshotFormatter.js +338 -0
- package/dist/src/snapshotFormatter.test.d.ts +7 -0
- package/dist/src/snapshotFormatter.test.js +521 -0
- package/dist/src/tui.d.ts +28 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/tui.test.d.ts +1 -0
- package/dist/src/tui.test.js +19 -0
- package/dist/src/types.d.ts +236 -0
- package/dist/src/types.js +7 -0
- package/dist/src/update-check.d.ts +66 -0
- package/dist/src/update-check.js +206 -0
- package/dist/src/update-check.test.d.ts +1 -0
- package/dist/src/update-check.test.js +117 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +152 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/dist/src/validate.d.ts +21 -0
- package/dist/src/validate.js +130 -0
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/dist/src/verify.d.ts +26 -0
- package/dist/src/verify.js +67 -0
- package/dist/src/version.d.ts +4 -0
- package/dist/src/version.js +22 -0
- package/dist/src/weixin-download.test.d.ts +1 -0
- package/dist/src/weixin-download.test.js +45 -0
- package/package.json +4 -3
- package/scripts/check-package-bin.mjs +95 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { buildTraceReceipt, exportObservationSession, getTraceDirectory } from './artifact.js';
|
|
6
|
+
import { ObservationSession } from './session.js';
|
|
7
|
+
describe('observation artifact', () => {
|
|
8
|
+
let baseDir;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-trace-'));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
fs.rmSync(baseDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
it('writes artifacts under profile-scoped trace directory', () => {
|
|
16
|
+
const session = new ObservationSession({
|
|
17
|
+
id: 'trace-1',
|
|
18
|
+
scope: {
|
|
19
|
+
contextId: 'work',
|
|
20
|
+
session: 'site:demo',
|
|
21
|
+
site: 'demo',
|
|
22
|
+
command: 'demo/run',
|
|
23
|
+
adapterSourcePath: '/tmp/clis/demo/run.js',
|
|
24
|
+
},
|
|
25
|
+
now: () => 1_700_000_000_000,
|
|
26
|
+
});
|
|
27
|
+
session.record({ stream: 'action', name: 'command', phase: 'start' });
|
|
28
|
+
session.record({ stream: 'screenshot', format: 'png', data: Buffer.from('png-bytes').toString('base64'), label: 'final' });
|
|
29
|
+
session.record({
|
|
30
|
+
stream: 'network',
|
|
31
|
+
url: 'https://api.test/data?token=secret',
|
|
32
|
+
method: 'GET',
|
|
33
|
+
status: 500,
|
|
34
|
+
requestHeaders: { authorization: 'Bearer secret' },
|
|
35
|
+
responseBody: { ok: false },
|
|
36
|
+
});
|
|
37
|
+
session.record({ stream: 'console', level: 'error', text: 'boom password=supersecret' });
|
|
38
|
+
const result = exportObservationSession(session, { baseDir, error: new Error('failed') });
|
|
39
|
+
expect(result.dir).toBe(getTraceDirectory('work', 'trace-1', baseDir));
|
|
40
|
+
expect(fs.existsSync(path.join(result.dir, 'trace.jsonl'))).toBe(true);
|
|
41
|
+
expect(fs.existsSync(path.join(result.dir, 'network.jsonl'))).toBe(true);
|
|
42
|
+
expect(fs.existsSync(path.join(result.dir, 'console.jsonl'))).toBe(true);
|
|
43
|
+
expect(fs.existsSync(result.receiptPath)).toBe(true);
|
|
44
|
+
expect(fs.readFileSync(path.join(result.dir, 'screenshots', '0001.png'), 'utf-8')).toBe('png-bytes');
|
|
45
|
+
const trace = fs.readFileSync(path.join(result.dir, 'trace.jsonl'), 'utf-8');
|
|
46
|
+
expect(trace).toContain('token=[REDACTED]');
|
|
47
|
+
expect(trace).toContain('"authorization":"[REDACTED]"');
|
|
48
|
+
expect(trace).not.toContain('supersecret');
|
|
49
|
+
const summary = fs.readFileSync(result.summaryPath, 'utf-8');
|
|
50
|
+
expect(summary).toContain('schemaVersion: 1');
|
|
51
|
+
expect(summary).toContain('webcmdVersion:');
|
|
52
|
+
expect(summary).toContain('expiresAt:');
|
|
53
|
+
expect(summary).toContain('status: failure');
|
|
54
|
+
expect(summary).toContain('contextId: "work"');
|
|
55
|
+
expect(summary).toContain('adapterSourcePath: "/tmp/clis/demo/run.js"');
|
|
56
|
+
expect(summary).toContain('adapterSourcePathExists: false');
|
|
57
|
+
expect(summary).toContain('## Failed Network');
|
|
58
|
+
expect(summary).toContain('500 GET https://api.test/data?token=[REDACTED]');
|
|
59
|
+
expect(summary).toContain('network: 1');
|
|
60
|
+
const receipt = JSON.parse(fs.readFileSync(result.receiptPath, 'utf-8'));
|
|
61
|
+
expect(receipt).toMatchObject({
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
webcmdVersion: expect.any(String),
|
|
64
|
+
traceId: 'trace-1',
|
|
65
|
+
traceDir: result.dir,
|
|
66
|
+
summaryPath: result.summaryPath,
|
|
67
|
+
receiptPath: result.receiptPath,
|
|
68
|
+
status: 'failure',
|
|
69
|
+
expiresAt: expect.any(String),
|
|
70
|
+
scope: {
|
|
71
|
+
contextId: 'work',
|
|
72
|
+
session: 'site:demo',
|
|
73
|
+
site: 'demo',
|
|
74
|
+
command: 'demo/run',
|
|
75
|
+
adapterSourcePath: '/tmp/clis/demo/run.js',
|
|
76
|
+
},
|
|
77
|
+
error: { message: 'failed' },
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it('builds a compact trace receipt', () => {
|
|
81
|
+
const receipt = buildTraceReceipt({
|
|
82
|
+
traceId: 'trace-1',
|
|
83
|
+
dir: '/tmp/webcmd/profiles/work/traces/trace-1',
|
|
84
|
+
summaryPath: '/tmp/webcmd/profiles/work/traces/trace-1/summary.md',
|
|
85
|
+
receiptPath: '/tmp/webcmd/profiles/work/traces/trace-1/receipt.json',
|
|
86
|
+
}, 'failure', new Error('failed with token=secret'), {
|
|
87
|
+
createdAt: '2026-05-03T00:00:00.000Z',
|
|
88
|
+
retentionPolicy: { maxAgeDays: 7 },
|
|
89
|
+
});
|
|
90
|
+
expect(receipt).toMatchObject({
|
|
91
|
+
schemaVersion: 1,
|
|
92
|
+
webcmdVersion: expect.any(String),
|
|
93
|
+
traceId: 'trace-1',
|
|
94
|
+
traceDir: '/tmp/webcmd/profiles/work/traces/trace-1',
|
|
95
|
+
receiptPath: '/tmp/webcmd/profiles/work/traces/trace-1/receipt.json',
|
|
96
|
+
status: 'failure',
|
|
97
|
+
expiresAt: '2026-05-10T00:00:00.000Z',
|
|
98
|
+
});
|
|
99
|
+
expect(receipt.error?.message).toContain('token=[REDACTED]');
|
|
100
|
+
});
|
|
101
|
+
it('prunes older traces after export while protecting the exported trace', () => {
|
|
102
|
+
const oldTraceDir = getTraceDirectory('work', 'old-trace', baseDir);
|
|
103
|
+
fs.mkdirSync(oldTraceDir, { recursive: true });
|
|
104
|
+
fs.writeFileSync(path.join(oldTraceDir, 'receipt.json'), JSON.stringify({
|
|
105
|
+
createdAt: '2026-04-01T00:00:00.000Z',
|
|
106
|
+
}), 'utf-8');
|
|
107
|
+
fs.writeFileSync(path.join(oldTraceDir, 'trace.jsonl'), '{}\n', 'utf-8');
|
|
108
|
+
const session = new ObservationSession({
|
|
109
|
+
id: 'new-trace',
|
|
110
|
+
scope: { contextId: 'work', session: 'site:demo' },
|
|
111
|
+
now: () => Date.parse('2026-05-03T00:00:00.000Z'),
|
|
112
|
+
});
|
|
113
|
+
session.record({ stream: 'action', name: 'command', phase: 'start' });
|
|
114
|
+
const result = exportObservationSession(session, {
|
|
115
|
+
baseDir,
|
|
116
|
+
retentionPolicy: { maxAgeDays: 365, maxCountPerProfile: 1, maxBytesPerProfile: '500MB' },
|
|
117
|
+
});
|
|
118
|
+
expect(fs.existsSync(oldTraceDir)).toBe(false);
|
|
119
|
+
expect(fs.existsSync(result.dir)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type ObservationStream = 'action' | 'network' | 'console' | 'screenshot' | 'state' | 'error';
|
|
2
|
+
export interface ObservationScope {
|
|
3
|
+
contextId?: string;
|
|
4
|
+
session: string;
|
|
5
|
+
target?: string;
|
|
6
|
+
site?: string;
|
|
7
|
+
command?: string;
|
|
8
|
+
adapterSourcePath?: string;
|
|
9
|
+
}
|
|
10
|
+
interface BaseObservationEvent {
|
|
11
|
+
id: string;
|
|
12
|
+
ts: number;
|
|
13
|
+
stream: ObservationStream;
|
|
14
|
+
}
|
|
15
|
+
export interface ActionObservationEvent extends BaseObservationEvent {
|
|
16
|
+
stream: 'action';
|
|
17
|
+
name: string;
|
|
18
|
+
phase?: 'start' | 'end' | 'error';
|
|
19
|
+
data?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface NetworkObservationEvent extends BaseObservationEvent {
|
|
22
|
+
stream: 'network';
|
|
23
|
+
url: string;
|
|
24
|
+
method?: string;
|
|
25
|
+
status?: number;
|
|
26
|
+
contentType?: string;
|
|
27
|
+
size?: number;
|
|
28
|
+
requestHeaders?: Record<string, unknown>;
|
|
29
|
+
responseHeaders?: Record<string, unknown>;
|
|
30
|
+
requestBody?: unknown;
|
|
31
|
+
responseBody?: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface ConsoleObservationEvent extends BaseObservationEvent {
|
|
34
|
+
stream: 'console';
|
|
35
|
+
level: string;
|
|
36
|
+
text: string;
|
|
37
|
+
source?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ScreenshotObservationEvent extends BaseObservationEvent {
|
|
40
|
+
stream: 'screenshot';
|
|
41
|
+
format: 'png' | 'jpeg';
|
|
42
|
+
data: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface StateObservationEvent extends BaseObservationEvent {
|
|
46
|
+
stream: 'state';
|
|
47
|
+
url?: string | null;
|
|
48
|
+
target?: string;
|
|
49
|
+
snapshot?: unknown;
|
|
50
|
+
label?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ErrorObservationEvent extends BaseObservationEvent {
|
|
53
|
+
stream: 'error';
|
|
54
|
+
code?: string;
|
|
55
|
+
message: string;
|
|
56
|
+
stack?: string;
|
|
57
|
+
hint?: string;
|
|
58
|
+
}
|
|
59
|
+
export type ObservationEvent = ActionObservationEvent | NetworkObservationEvent | ConsoleObservationEvent | ScreenshotObservationEvent | StateObservationEvent | ErrorObservationEvent;
|
|
60
|
+
export type ObservationEventInput = ObservationEvent extends infer T ? T extends ObservationEvent ? Omit<T, 'id' | 'ts'> & Partial<Pick<T, 'id' | 'ts'>> : never : never;
|
|
61
|
+
export interface ObservationExportResult {
|
|
62
|
+
traceId: string;
|
|
63
|
+
dir: string;
|
|
64
|
+
summaryPath: string;
|
|
65
|
+
receiptPath: string;
|
|
66
|
+
receipt: ObservationTraceReceipt;
|
|
67
|
+
}
|
|
68
|
+
export type ObservationExportStatus = 'success' | 'failure';
|
|
69
|
+
export interface ObservationTraceReceipt {
|
|
70
|
+
schemaVersion: 1;
|
|
71
|
+
webcmdVersion: string;
|
|
72
|
+
traceId: string;
|
|
73
|
+
traceDir: string;
|
|
74
|
+
summaryPath: string;
|
|
75
|
+
receiptPath: string;
|
|
76
|
+
status: ObservationExportStatus;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
/** Advisory only; actual deletion is governed by current trace retention budgets. */
|
|
79
|
+
expiresAt?: string;
|
|
80
|
+
scope?: ObservationScope;
|
|
81
|
+
error?: {
|
|
82
|
+
name?: string;
|
|
83
|
+
code?: string;
|
|
84
|
+
message: string;
|
|
85
|
+
hint?: string;
|
|
86
|
+
exitCode?: number;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ObservationSession, type ObservationSessionOptions } from './session.js';
|
|
2
|
+
import type { ObservationScope } from './events.js';
|
|
3
|
+
export declare class ObservationManager {
|
|
4
|
+
private readonly sessions;
|
|
5
|
+
start(opts: ObservationSessionOptions): ObservationSession;
|
|
6
|
+
get(id: string): ObservationSession | undefined;
|
|
7
|
+
stop(id: string): ObservationSession | undefined;
|
|
8
|
+
findByScope(scope: ObservationScope): ObservationSession[];
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ObservationSession } from './session.js';
|
|
2
|
+
export class ObservationManager {
|
|
3
|
+
sessions = new Map();
|
|
4
|
+
start(opts) {
|
|
5
|
+
const session = new ObservationSession(opts);
|
|
6
|
+
this.sessions.set(session.id, session);
|
|
7
|
+
return session;
|
|
8
|
+
}
|
|
9
|
+
get(id) {
|
|
10
|
+
return this.sessions.get(id);
|
|
11
|
+
}
|
|
12
|
+
stop(id) {
|
|
13
|
+
const session = this.sessions.get(id);
|
|
14
|
+
this.sessions.delete(id);
|
|
15
|
+
return session;
|
|
16
|
+
}
|
|
17
|
+
findByScope(scope) {
|
|
18
|
+
return [...this.sessions.values()].filter((session) => scopeMatches(session.scope, scope));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function scopeMatches(actual, expected) {
|
|
22
|
+
return actual.session === expected.session
|
|
23
|
+
&& (expected.contextId === undefined || actual.contextId === expected.contextId)
|
|
24
|
+
&& (expected.target === undefined || actual.target === expected.target)
|
|
25
|
+
&& (expected.site === undefined || actual.site === expected.site)
|
|
26
|
+
&& (expected.command === undefined || actual.command === expected.command);
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ObservationManager } from './manager.js';
|
|
3
|
+
describe('ObservationManager', () => {
|
|
4
|
+
it('indexes sessions by id and scope', () => {
|
|
5
|
+
const manager = new ObservationManager();
|
|
6
|
+
const work = manager.start({ id: 'work-1', scope: { contextId: 'work', session: 'site:x', target: 'tab-1' } });
|
|
7
|
+
manager.start({ id: 'personal-1', scope: { contextId: 'personal', session: 'site:x', target: 'tab-2' } });
|
|
8
|
+
expect(manager.get('work-1')).toBe(work);
|
|
9
|
+
expect(manager.findByScope({ contextId: 'work', session: 'site:x' }).map((session) => session.id)).toEqual(['work-1']);
|
|
10
|
+
expect(manager.stop('work-1')).toBe(work);
|
|
11
|
+
expect(manager.get('work-1')).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RedactionOptions {
|
|
2
|
+
allowlist?: string[];
|
|
3
|
+
maxStringLength?: number;
|
|
4
|
+
maxDepth?: number;
|
|
5
|
+
maxArrayItems?: number;
|
|
6
|
+
maxObjectFields?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function redactUrl(url: string): string;
|
|
9
|
+
export declare function redactHeaders(headers: Record<string, unknown> | undefined, opts?: RedactionOptions): Record<string, unknown> | undefined;
|
|
10
|
+
export declare function redactText(text: string, opts?: RedactionOptions): string;
|
|
11
|
+
export declare function redactValue(value: unknown, opts?: RedactionOptions, keyHint?: string, depth?: number): unknown;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const DEFAULT_REDACTION = '[REDACTED]';
|
|
2
|
+
const SENSITIVE_HEADER_NAMES = new Set([
|
|
3
|
+
'authorization',
|
|
4
|
+
'cookie',
|
|
5
|
+
'set-cookie',
|
|
6
|
+
'proxy-authorization',
|
|
7
|
+
'x-api-key',
|
|
8
|
+
'x-auth-token',
|
|
9
|
+
'x-csrf-token',
|
|
10
|
+
'x-xsrf-token',
|
|
11
|
+
]);
|
|
12
|
+
const SENSITIVE_FIELD_PATTERN = /(password|passwd|pwd|token|secret|authorization|cookie|set-cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|session[_-]?id|csrf|xsrf)/i;
|
|
13
|
+
const SENSITIVE_URL_PARAMS = /([?&])(token|key|secret|password|auth|access_token|api_key|session_id|csrf|xsrf)=[^&]*/gi;
|
|
14
|
+
export function redactUrl(url) {
|
|
15
|
+
return url.replace(SENSITIVE_URL_PARAMS, '$1$2=[REDACTED]');
|
|
16
|
+
}
|
|
17
|
+
export function redactHeaders(headers, opts = {}) {
|
|
18
|
+
if (!headers)
|
|
19
|
+
return headers;
|
|
20
|
+
const allow = new Set((opts.allowlist ?? []).map((key) => key.toLowerCase()));
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
23
|
+
const lower = key.toLowerCase();
|
|
24
|
+
out[key] = SENSITIVE_HEADER_NAMES.has(lower) && !allow.has(lower)
|
|
25
|
+
? DEFAULT_REDACTION
|
|
26
|
+
: redactValue(value, opts, key);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
export function redactText(text, opts = {}) {
|
|
31
|
+
const max = opts.maxStringLength ?? 50_000;
|
|
32
|
+
let out = text
|
|
33
|
+
.replace(/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, 'Bearer [REDACTED]')
|
|
34
|
+
.replace(/(["'])(password|passwd|pwd|token|secret|api_key|apikey|access_token|session_id)\1\s*:\s*(["'])(.*?)\3/gi, '$1$2$1:$3[REDACTED]$3')
|
|
35
|
+
.replace(/(token|secret|password|api_key|apikey|access_token|session_id)[=:]\s*['"]?[^'"\s,;}&]+['"]?/gi, '$1=[REDACTED]')
|
|
36
|
+
.replace(/(cookie[=:]\s*)[^\n;]{3,}/gi, '$1[REDACTED]')
|
|
37
|
+
.replace(/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, '[REDACTED_JWT]');
|
|
38
|
+
if (out.length > max)
|
|
39
|
+
out = out.slice(0, max) + `\n...[truncated, ${out.length - max} chars omitted]`;
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
export function redactValue(value, opts = {}, keyHint, depth = 0) {
|
|
43
|
+
const allow = new Set((opts.allowlist ?? []).map((key) => key.toLowerCase()));
|
|
44
|
+
if (keyHint && SENSITIVE_FIELD_PATTERN.test(keyHint) && !allow.has(keyHint.toLowerCase())) {
|
|
45
|
+
return DEFAULT_REDACTION;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
return keyHint === 'url' ? redactUrl(value) : redactText(value, opts);
|
|
49
|
+
}
|
|
50
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean')
|
|
51
|
+
return value;
|
|
52
|
+
const maxDepth = opts.maxDepth ?? 5;
|
|
53
|
+
if (depth >= maxDepth)
|
|
54
|
+
return '[truncated: max depth reached]';
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
const max = opts.maxArrayItems ?? 100;
|
|
57
|
+
const items = value.slice(0, max).map((item) => redactValue(item, opts, undefined, depth + 1));
|
|
58
|
+
if (value.length > max)
|
|
59
|
+
items.push(`[truncated, ${value.length - max} items omitted]`);
|
|
60
|
+
return items;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'object') {
|
|
63
|
+
const entries = Object.entries(value);
|
|
64
|
+
const max = opts.maxObjectFields ?? 100;
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [key, child] of entries.slice(0, max)) {
|
|
67
|
+
if (key.toLowerCase() === 'url' && typeof child === 'string')
|
|
68
|
+
out[key] = redactUrl(child);
|
|
69
|
+
else if (key.toLowerCase().includes('headers') && child && typeof child === 'object' && !Array.isArray(child)) {
|
|
70
|
+
out[key] = redactHeaders(child, opts);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out[key] = redactValue(child, opts, key, depth + 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (entries.length > max)
|
|
77
|
+
out.__truncated__ = `[${entries.length - max} fields omitted]`;
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { redactHeaders, redactUrl, redactValue } from './redaction.js';
|
|
3
|
+
describe('observation redaction', () => {
|
|
4
|
+
it('redacts sensitive headers by default', () => {
|
|
5
|
+
expect(redactHeaders({
|
|
6
|
+
authorization: 'Bearer secret-token',
|
|
7
|
+
cookie: 'sid=abc',
|
|
8
|
+
'set-cookie': 'sid=abc',
|
|
9
|
+
accept: 'application/json',
|
|
10
|
+
})).toEqual({
|
|
11
|
+
authorization: '[REDACTED]',
|
|
12
|
+
cookie: '[REDACTED]',
|
|
13
|
+
'set-cookie': '[REDACTED]',
|
|
14
|
+
accept: 'application/json',
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
it('redacts sensitive url query params', () => {
|
|
18
|
+
expect(redactUrl('https://x.test/api?token=abc&ok=1&password=secret'))
|
|
19
|
+
.toBe('https://x.test/api?token=[REDACTED]&ok=1&password=[REDACTED]');
|
|
20
|
+
});
|
|
21
|
+
it('redacts password and token fields recursively', () => {
|
|
22
|
+
expect(redactValue({
|
|
23
|
+
user: 'alice',
|
|
24
|
+
password: 'secret',
|
|
25
|
+
nested: { access_token: 'abc123456789', value: 'safe' },
|
|
26
|
+
})).toEqual({
|
|
27
|
+
user: 'alice',
|
|
28
|
+
password: '[REDACTED]',
|
|
29
|
+
nested: { access_token: '[REDACTED]', value: 'safe' },
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface TraceRetentionPolicyInput {
|
|
2
|
+
maxAgeDays?: number;
|
|
3
|
+
maxCountPerProfile?: number;
|
|
4
|
+
maxBytesPerProfile?: string | number;
|
|
5
|
+
}
|
|
6
|
+
export interface ResolvedTraceRetentionPolicy {
|
|
7
|
+
maxAgeDays: number;
|
|
8
|
+
maxAgeMs: number;
|
|
9
|
+
maxCountPerProfile: number;
|
|
10
|
+
maxBytesPerProfile: number;
|
|
11
|
+
}
|
|
12
|
+
export interface TraceRetentionPruneResult {
|
|
13
|
+
scanned: number;
|
|
14
|
+
deleted: string[];
|
|
15
|
+
kept: string[];
|
|
16
|
+
totalBytesBefore: number;
|
|
17
|
+
totalBytesAfter: number;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_TRACE_RETENTION_POLICY: {
|
|
20
|
+
maxAgeDays: number;
|
|
21
|
+
maxCountPerProfile: number;
|
|
22
|
+
maxBytesPerProfile: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function parseByteSize(value: string | number): number;
|
|
25
|
+
export declare function resolveTraceRetentionPolicy(input?: TraceRetentionPolicyInput): ResolvedTraceRetentionPolicy;
|
|
26
|
+
export declare function traceExpiresAt(createdAt: string, policyInput?: TraceRetentionPolicyInput): string;
|
|
27
|
+
export declare function pruneTraceArtifacts(tracesDir: string, opts?: {
|
|
28
|
+
policy?: TraceRetentionPolicyInput;
|
|
29
|
+
protectedTraceDirs?: string[];
|
|
30
|
+
now?: () => number;
|
|
31
|
+
warn?: (message: string) => void;
|
|
32
|
+
}): TraceRetentionPruneResult;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { log } from '../logger.js';
|
|
4
|
+
export const DEFAULT_TRACE_RETENTION_POLICY = {
|
|
5
|
+
maxAgeDays: 7,
|
|
6
|
+
maxCountPerProfile: 20,
|
|
7
|
+
maxBytesPerProfile: '500MB',
|
|
8
|
+
};
|
|
9
|
+
const BYTES_UNITS = {
|
|
10
|
+
B: 1,
|
|
11
|
+
KB: 1024,
|
|
12
|
+
MB: 1024 ** 2,
|
|
13
|
+
GB: 1024 ** 3,
|
|
14
|
+
};
|
|
15
|
+
export function parseByteSize(value) {
|
|
16
|
+
if (typeof value === 'number') {
|
|
17
|
+
if (!Number.isFinite(value) || value < 0)
|
|
18
|
+
throw new Error(`Invalid byte size: ${value}`);
|
|
19
|
+
return Math.floor(value);
|
|
20
|
+
}
|
|
21
|
+
const match = value.trim().match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)?$/i);
|
|
22
|
+
if (!match)
|
|
23
|
+
throw new Error(`Invalid byte size: ${value}`);
|
|
24
|
+
const amount = Number(match[1]);
|
|
25
|
+
const unit = (match[2] ?? 'B').toUpperCase();
|
|
26
|
+
return Math.floor(amount * BYTES_UNITS[unit]);
|
|
27
|
+
}
|
|
28
|
+
export function resolveTraceRetentionPolicy(input = {}) {
|
|
29
|
+
const maxAgeDays = input.maxAgeDays ?? DEFAULT_TRACE_RETENTION_POLICY.maxAgeDays;
|
|
30
|
+
const maxCountPerProfile = input.maxCountPerProfile ?? DEFAULT_TRACE_RETENTION_POLICY.maxCountPerProfile;
|
|
31
|
+
if (!Number.isFinite(maxAgeDays) || maxAgeDays < 0)
|
|
32
|
+
throw new Error(`Invalid trace maxAgeDays: ${maxAgeDays}`);
|
|
33
|
+
if (!Number.isInteger(maxCountPerProfile) || maxCountPerProfile < 0) {
|
|
34
|
+
throw new Error(`Invalid trace maxCountPerProfile: ${maxCountPerProfile}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
maxAgeDays,
|
|
38
|
+
maxAgeMs: maxAgeDays * 24 * 60 * 60 * 1000,
|
|
39
|
+
maxCountPerProfile,
|
|
40
|
+
maxBytesPerProfile: parseByteSize(input.maxBytesPerProfile ?? DEFAULT_TRACE_RETENTION_POLICY.maxBytesPerProfile),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function traceExpiresAt(createdAt, policyInput = {}) {
|
|
44
|
+
const policy = resolveTraceRetentionPolicy(policyInput);
|
|
45
|
+
const createdAtMs = Date.parse(createdAt);
|
|
46
|
+
const base = Number.isFinite(createdAtMs) ? createdAtMs : Date.now();
|
|
47
|
+
return new Date(base + policy.maxAgeMs).toISOString();
|
|
48
|
+
}
|
|
49
|
+
export function pruneTraceArtifacts(tracesDir, opts = {}) {
|
|
50
|
+
const warn = opts.warn ?? ((message) => log.warn(`[trace] ${message}`));
|
|
51
|
+
const policy = resolveTraceRetentionPolicy(opts.policy);
|
|
52
|
+
const now = opts.now ?? Date.now;
|
|
53
|
+
const protectedDirs = new Set((opts.protectedTraceDirs ?? []).map((dir) => path.resolve(dir)));
|
|
54
|
+
const entries = readTraceEntries(tracesDir, protectedDirs, warn);
|
|
55
|
+
const totalBytesBefore = entries.reduce((sum, entry) => sum + entry.sizeBytes, 0);
|
|
56
|
+
const deleted = new Set();
|
|
57
|
+
const sorted = [...entries].sort((a, b) => a.createdAtMs - b.createdAtMs || a.dir.localeCompare(b.dir));
|
|
58
|
+
const cutoff = now() - policy.maxAgeMs;
|
|
59
|
+
for (const entry of sorted) {
|
|
60
|
+
if (!entry.protected && entry.createdAtMs < cutoff)
|
|
61
|
+
deleted.add(entry.dir);
|
|
62
|
+
}
|
|
63
|
+
let remaining = sorted.filter((entry) => !deleted.has(entry.dir));
|
|
64
|
+
while (remaining.length > policy.maxCountPerProfile) {
|
|
65
|
+
const victim = remaining.find((entry) => !entry.protected);
|
|
66
|
+
if (!victim)
|
|
67
|
+
break;
|
|
68
|
+
deleted.add(victim.dir);
|
|
69
|
+
remaining = remaining.filter((entry) => entry.dir !== victim.dir);
|
|
70
|
+
}
|
|
71
|
+
let remainingBytes = remaining.reduce((sum, entry) => sum + entry.sizeBytes, 0);
|
|
72
|
+
while (remainingBytes > policy.maxBytesPerProfile) {
|
|
73
|
+
const victim = remaining.find((entry) => !entry.protected);
|
|
74
|
+
if (!victim)
|
|
75
|
+
break;
|
|
76
|
+
deleted.add(victim.dir);
|
|
77
|
+
remaining = remaining.filter((entry) => entry.dir !== victim.dir);
|
|
78
|
+
remainingBytes -= victim.sizeBytes;
|
|
79
|
+
}
|
|
80
|
+
const deletedDirs = [];
|
|
81
|
+
for (const dir of sorted.map((entry) => entry.dir).filter((dir) => deleted.has(dir))) {
|
|
82
|
+
try {
|
|
83
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
deletedDirs.push(dir);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
warn(`Failed to prune trace artifact ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const keptEntries = entries.filter((entry) => !deletedDirs.includes(entry.dir));
|
|
91
|
+
return {
|
|
92
|
+
scanned: entries.length,
|
|
93
|
+
deleted: deletedDirs,
|
|
94
|
+
kept: keptEntries.map((entry) => entry.dir),
|
|
95
|
+
totalBytesBefore,
|
|
96
|
+
totalBytesAfter: keptEntries.reduce((sum, entry) => sum + entry.sizeBytes, 0),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function readTraceEntries(tracesDir, protectedDirs, warn) {
|
|
100
|
+
let names;
|
|
101
|
+
try {
|
|
102
|
+
names = fs.readdirSync(tracesDir);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (isEnoent(err))
|
|
106
|
+
return [];
|
|
107
|
+
warn(`Failed to list trace artifacts in ${tracesDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const entries = [];
|
|
111
|
+
for (const name of names) {
|
|
112
|
+
const dir = path.join(tracesDir, name);
|
|
113
|
+
try {
|
|
114
|
+
const stat = fs.statSync(dir);
|
|
115
|
+
if (!stat.isDirectory())
|
|
116
|
+
continue;
|
|
117
|
+
entries.push({
|
|
118
|
+
dir,
|
|
119
|
+
createdAtMs: readCreatedAtMs(dir, stat.mtimeMs),
|
|
120
|
+
sizeBytes: directorySize(dir),
|
|
121
|
+
protected: protectedDirs.has(path.resolve(dir)),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (!isEnoent(err)) {
|
|
126
|
+
warn(`Failed to inspect trace artifact ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return entries;
|
|
131
|
+
}
|
|
132
|
+
function readCreatedAtMs(dir, fallbackMs) {
|
|
133
|
+
try {
|
|
134
|
+
const receipt = JSON.parse(fs.readFileSync(path.join(dir, 'receipt.json'), 'utf-8'));
|
|
135
|
+
if (typeof receipt.createdAt === 'string') {
|
|
136
|
+
const parsed = Date.parse(receipt.createdAt);
|
|
137
|
+
if (Number.isFinite(parsed))
|
|
138
|
+
return parsed;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Older or hand-edited trace directories may not have a receipt.
|
|
143
|
+
}
|
|
144
|
+
return fallbackMs;
|
|
145
|
+
}
|
|
146
|
+
function directorySize(dir) {
|
|
147
|
+
let total = 0;
|
|
148
|
+
for (const name of fs.readdirSync(dir)) {
|
|
149
|
+
const item = path.join(dir, name);
|
|
150
|
+
const stat = fs.lstatSync(item);
|
|
151
|
+
if (stat.isDirectory())
|
|
152
|
+
total += directorySize(item);
|
|
153
|
+
else
|
|
154
|
+
total += stat.size;
|
|
155
|
+
}
|
|
156
|
+
return total;
|
|
157
|
+
}
|
|
158
|
+
function isEnoent(err) {
|
|
159
|
+
return typeof err === 'object' && err !== null && 'code' in err && err.code === 'ENOENT';
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|