[
  {
    "path": ".cursor/rules/caido-backend.mdc",
    "content": "---\nglobs:\n  - \"**/packages/backend/**\"\nalwaysApply: true\ndescription: Caido Backend SDK Rules and Patterns\n---\n\n## Caido Backend SDK\n\n### Overview\n\nThe Caido Backend SDK is used for server-side logic, data processing, and creating API endpoints that can be called from frontend plugins.\n\n### Entry Point\n\nBackend plugins are initialized via `packages/backend/src/index.ts`:\n\n```typescript\nimport { SDK, DefineAPI } from \"caido:plugin\";\n\n// Define your API functions\nfunction myCustomFunction(sdk: SDK, param: string) {\n  sdk.console.log(`Called with: ${param}`);\n  return `Processed: ${param}`;\n}\n\n// Export the API type definition\nexport type API = DefineAPI<{\n  myCustomFunction: typeof myCustomFunction;\n}>;\n\n// Plugin initialization\nexport function init(sdk: SDK<API>) {\n  // Register API endpoints\n  sdk.api.register(\"myCustomFunction\", myCustomFunction);\n}\n```\n\n### SDK Type Definitions\n\n#### Backend SDK with events:\n```typescript\nimport { DefineEvents, SDK } from \"caido:plugin\";\n\nexport type BackendEvents = DefineEvents<{\n  \"data-updated\": { message: string };\n  \"status-changed\": { status: \"active\" | \"inactive\" };\n}>;\n\nexport type CaidoBackendSDK = SDK<never, BackendEvents>;\n```\n\n### Best Practices\n\nWhen building API endpoints in the backend and calling them from the frontend, use Result types to handle errors gracefully without throwing exceptions:\n\n```typescript\n// Define the Result type\nexport type Result<T> =\n  | { kind: \"Error\"; error: string }\n  | { kind: \"Ok\"; value: T };\n\n// Backend API function returning Result\nfunction processData(sdk: SDK, input: string): Result<ProcessedData> {\n  try {\n    // Your processing logic here\n    const processed = doSomeProcessing(input);\n    return { kind: \"Ok\", value: processed };\n  } catch (error) {\n    return { kind: \"Error\", error: error.message };\n  }\n}\n\n// Frontend usage - no try/catch needed\nconst handleProcess = async () => {\n  const result = await sdk.backend.processData(inputValue);\n\n  if (result.kind === \"Error\") {\n    sdk.window.showToast(result.error, { variant: \"error\" });\n    return;\n  }\n\n  // Handle successful result\n  const data = result.value;\n  sdk.window.showToast(\"Processing completed!\", { variant: \"success\" });\n};\n```\n\n\n#### Registering Multiple API Endpoints\n\n```typescript\n// Define multiple API functions\nfunction getData(sdk: SDK, id: string): Result<Data> {\n  // Implementation\n}\n\nfunction saveData(sdk: SDK, data: Data): Result<void> {\n  // Implementation\n}\n\nfunction deleteData(sdk: SDK, id: string): Result<boolean> {\n  // Implementation\n}\n\n// Export Caido Backend API\nexport type API = DefineAPI<{\n  getData: typeof getData;\n  saveData: typeof saveData;\n  deleteData: typeof deleteData;\n}>;\n\n// Register all endpoints\nexport function init(sdk: SDK<API>) {\n  sdk.api.register(\"getData\", getData);\n  sdk.api.register(\"saveData\", saveData);\n  sdk.api.register(\"deleteData\", deleteData);\n}\n```\n"
  },
  {
    "path": ".cursor/rules/caido-frontend.mdc",
    "content": "---\nglobs:\n  - \"**/packages/frontend/**\"\nalwaysApply: true\ndescription: Caido Frontend SDK Rules and Patterns\n---\n\n## Caido Frontend SDK\n\n### Overview\n\nThe Caido Frontend SDK is used for creating UI components, pages, and handling user interactions in Caido plugins.\n\n### Entry Point\n\nFrontend plugins are initialized via `packages/frontend/src/index.ts`:\n\n```typescript\nimport { Caido } from \"@caido/sdk-frontend\";\nimport { API, BackendEvents } from \"backend\";\n\n// Define SDK type with backend API\nexport type FrontendSDK = Caido<API, BackendEvents>;\n\n// Plugin initialization\nexport const init = (sdk: FrontendSDK) => {\n  // Create pages and UI\n  createPage(sdk);\n\n  // Register sidebar items\n  sdk.sidebar.registerItem(\"My Plugin\", \"/my-plugin-page\", {\n    icon: \"fas fa-rocket\"\n  });\n\n  // Register commands\n  sdk.commands.register(\"my-command\", {\n    name: \"My Custom Command\",\n    run: () => sdk.backend.myCustomFunction(\"Hello\"),\n  });\n};\n```\n\n### SDK Type Definitions\n\n#### For plugins WITHOUT backend, this is fine:\n```typescript\nexport type FrontendSDK = Caido<Record<string, never>, Record<string, never>>;\n```\n\n#### For plugins WITH backend:\n```typescript\nimport { Caido } from \"@caido/sdk-frontend\";\nimport { API, BackendEvents } from \"backend\";\n\nexport type FrontendSDK = Caido<API, BackendEvents>;\n```\n\n### Command Pattern\n\nCommands provide a unified way to register actions that can be triggered from:\n- Command palette (Ctrl/Cmd+Shift+P)\n- Context menus (right-click)\n- UI buttons\n- Keyboard shortcuts\n\nCommands is a frontend-only concept.\n\n```typescript\n// Define command IDs as constants\nconst Commands = {\n  processData: \"my-plugin.process-data\",\n  exportResults: \"my-plugin.export-results\",\n} as const;\n\n// Register commands\nsdk.commands.register(Commands.processData, {\n  name: \"Process Data\",\n  run: async () => {\n    const result = await sdk.backend.processData();\n    sdk.window.showToast(`Processed ${result.count} items`, {\n      variant: \"success\"\n    });\n  },\n  group: \"My Plugin\",\n});\n\n// Add to command palette\nsdk.commandPalette.register(Commands.processData);\n\n// Add to context menus\nsdk.menu.registerItem({\n  type: \"Request\",\n  commandId: Commands.processData,\n  leadingIcon: \"fas fa-cog\",\n});\n```\n\n### Working with Requests and Responses\n\n#### Creating and Sending Requests\n\n```typescript\nimport { RequestSpec } from \"caido:utils\";\nimport { type Request, type Response } from \"caido:utils\";\n\n// Create a new request\nconst spec = new RequestSpec(\"https://api.example.com/data\");\nspec.setMethod(\"POST\");\nspec.setHeader(\"Content-Type\", \"application/json\");\nspec.setBody(JSON.stringify({ key: \"value\" }));\n\n// Send the request\nconst result = await sdk.requests.send(spec);\nif (result.response) {\n  const statusCode = result.response.getCode();\n  const responseBody = result.response.getBody()?.toText();\n}\n```\n\n#### Working with Request/Response Editors\n\n```typescript\n// Create editors for viewing/editing HTTP data\nconst reqEditor = sdk.ui.httpRequestEditor();\nconst respEditor = sdk.ui.httpResponseEditor();\n\n// Get DOM elements\nconst reqElement = reqEditor.getElement();\nconst respElement = respEditor.getElement();\n\n// Style and layout\nreqElement.style.width = \"50%\";\nrespElement.style.width = \"50%\";\n\nconst editorsContainer = document.createElement(\"div\");\neditorsContainer.style.display = \"flex\";\neditorsContainer.appendChild(reqElement);\neditorsContainer.appendChild(respElement);\n```\n\n### Frontend Error Handling\n\nWhen calling backend APIs from the frontend, handle Result types gracefully:\n\n```typescript\n// Frontend usage - no try/catch needed\nconst handleProcess = async () => {\n  const result = await sdk.backend.processData(inputValue);\n\n  if (result.kind === \"Error\") {\n    sdk.window.showToast(result.error, { variant: \"error\" });\n    return;\n  }\n\n  // Handle successful result\n  const data = result.value;\n  sdk.window.showToast(\"Processing completed!\", { variant: \"success\" });\n};\n```\n"
  },
  {
    "path": ".cursor/rules/caido.mdc",
    "content": "---\nglobs:\nalwaysApply: true\ndescription: Caido HTTP Proxy Overview\n---\n\n## What is Caido\n\nCaido is a lightweight web application security auditing toolkit designed to help security professionals audit web applications with efficiency and ease\n\nKey features include:\n  - HTTP proxy for intercepting and viewing requests in real-time\n  - Replay functionality for resending and modifying requests to test endpoints\n  - Automate feature for testing requests against wordlists\n  - Match & Replace for automatically modifying requests with regex rules\n  - HTTPQL query language for filtering through HTTP traffic\n  - Workflow system for creating custom encoders/decoders and plugins\n  - Project management for organizing different security assessments\n\n## Environment\n\nWe are running in a plugin environment where we can interact with Caido through the Caido Backend or Frontend SDK.\n\n### Plugin Structure\n\n- `packages/backend` → Backend plugin code - handles server-side logic, data processing, and API endpoints\n- `packages/frontend` → Frontend plugin code - handles UI components, user interactions, and calls to backend\n\n### Plugin Development\n\nPlugins consist of:\n- A `caido.config.ts` configuration file\n- Frontend plugin (optional) - provides UI using Caido Frontend SDK\n- Backend plugin (optional) - provides server-side functionality using Caido Backend SDK\n\nThese are packaged together as a single plugin package that can be installed in Caido.\n\n### Key Development Concepts\n\n- Frontend plugins create pages, UI components, and handle user interactions\n- Backend plugins register API endpoints that can be called from frontend\n- Communication between frontend and backend happens through registered API calls\n\n### Caido Findings SDK\n\nFindings allow you to create alerts when Caido detects notable characteristics in requests/responses based on conditional statements. When triggered, they generate alerts to draw attention to interesting traffic.\n\nExample - Create a finding for successful responses:\n```typescript\nawait sdk.findings.create({\n  title: `Success Response ${response.getCode()}`,\n  description: `Request ID: ${request.getId()}\\nResponse Code: ${response.getCode()}`,\n  reporter: \"Response Logger Plugin\",\n  request: request,\n  dedupeKey: `${request.getPath()}-${response.getCode()}` // Prevents duplicates\n});\n```\n\n### Important Caido SDK Types\n\n```typescript\nexport type Request = {\n  getId(): ID;\n  getHost(): string;\n  getPort(): number;\n  getTls(): boolean;\n  getMethod(): string;\n  getPath(): string;\n  getQuery(): string;\n  getUrl(): string;\n  getHeaders(): Record<string, Array<string>>;\n  getHeader(name: string): Array<string> | undefined;\n  getBody(): Body | undefined;\n  getRaw(): RequestRaw;\n  getCreatedAt(): Date;\n  toSpec(): RequestSpec;\n  toSpecRaw(): RequestSpecRaw;\n};\n\nexport type Response = {\n  getId(): ID;\n  getCode(): number;\n  getHeaders(): Record<string, Array<string>>;\n  getHeader(name: string): Array<string> | undefined;\n  getBody(): Body | undefined;\n  getRaw(): ResponseRaw;\n  getRoundtripTime(): number;\n  getCreatedAt(): Date;\n};\n```\n\nFor Body and Raw you can use methods like `getBody()?.toText()` to extract text content.\n\nThese types can be imported by:\n```\nimport { type Request, type Response } from \"caido:utils\";\n```\n"
  },
  {
    "path": ".cursor/rules/linter.mdc",
    "content": "---\nglobs:\nalwaysApply: true\ndescription: Linter Guidelines\n---\n\n# Linter\n\nWe have a built-in ESLint linter configured at the root folder. After making any significant change, always run the linter with `pnpm lint` and fix all potential issues.\n\n## Most common mistakes that lead to linter errors\n\n### Lint Rule: Unexpected nullable string value in conditional. Please handle nullish or empty cases explicitly\n\nTo prevent this, when comparing strings, instead of writing:\n\n```\nif (!str) {}\n```\n\ndo this:\n\n```\nif (str !== undefined) {}\n```\n"
  },
  {
    "path": ".cursor/rules/style.mdc",
    "content": "---\nglobs: **/**.vue\nalwaysApply: false\n---\n## UI Style Guidelines\n\n### PrimeVue\n\n- Prefer to use PrimeVue compontents where possible\n- A custom PrimeVue theme is configured with dark mode as default, handling most color-related styles for us.\n\n### General Theme\n\n- Dark Mode is default — all UI elements follow a dark, low-contrast background with light text for high readability.\n- For text / background colors, prefer to use `...-surface-...` f.e. `border-surface-700`.\n- Caido uses `bg-surface-800` as the main app background, `bg-surface-700` is the background used for `Card` component.\n- Follow minimalistic color palette. AVOID using too much colors where possible.\n\n### Layout & Components\n\n- Often use PrimeVue `Splitter` and `SplitterPanel` for vertical or horizontal layout.\n- Prefer to use `Card` PrimeVue components a lot, if needed add `h-full` to them via `pt` params.\n\nExample:\n\n```\n<Card\n  class=\"h-full\"\n  :pt=\"{\n    body: { class: 'h-full p-0' },\n    content: { class: 'h-full flex flex-col' },\n  }\"\n>\n```\n\n### Enviroment\n\n- Keep in mind that we are building a plugin that's inside a Caido web app, we can modify frontend by adding sidebar pages using Caido Frontend SDK.\n- Our plugin content is rendered within a dedicated window/panel that Caido provides for our sidebar page.\n- The plugin UI should integrate seamlessly with Caido's existing interface and theming.\n\n### Data Representation\n\n- Prefer `DataTable` component for displaying structured data:\n  * Caido often uses `stripedRows`, use it where possible\n  * Actions column at the end (e.g. “Install” buttons).\n- Empty states use friendly, minimal messages with icons.\n\n### Icons\n\n- Always use `fas fa-[...]` for icons. We don't support any other icon libraries.\n"
  },
  {
    "path": ".cursor/rules/typescript.mdc",
    "content": "---\nglobs:\nalwaysApply: true\ndescription: TypeScript Guidelines\n---\n# TypeScript Guidelines\n\n- Use TypeScript for all files.\n- NEVER use `any` type.\n- Use `undefined` over `null`.\n- Try to keep things in one function unless composable or reusable.\n- Prefer single word variable names where possible.\n- DO NOT do unnecessary destructuring of variables.\n- AVOID `else` statements where possible.\n- AVOID `try` / `catch` where possible.\n- AVOID using interfaces where possible.\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: 🚀 Release\n\non:\n  workflow_dispatch:\n\nenv:\n  NODE_VERSION: 20\n  PNPM_VERSION: 9\n\njobs:\n  release:\n    name: Release\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n\n    steps:\n      - name: Checkout project\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n        with:\n          version: ${{ env.PNPM_VERSION }}\n          run_install: true\n\n      - name: Build package\n        run: pnpm build\n\n      - name: Sign package\n        working-directory: dist\n        run: |\n          if [[ -z \"${{ secrets.PRIVATE_KEY }}\" ]]; then\n            echo \"Set an ed25519 key as PRIVATE_KEY in GitHub Action secret to sign.\"\n          else\n            echo \"${{ secrets.PRIVATE_KEY }}\" > private_key.pem\n            openssl pkeyutl -sign -inkey private_key.pem -out plugin_package.zip.sig -rawin -in plugin_package.zip\n            rm private_key.pem\n          fi\n\n      - name: Check version\n        id: meta\n        working-directory: dist\n        run: |\n          VERSION=$(unzip -p plugin_package.zip manifest.json | jq -r .version)\n          echo \"version=${VERSION}\" >> $GITHUB_OUTPUT\n\n      - name: Create release\n        uses: caido/action-release@v1\n        with:\n          tag: ${{ steps.meta.outputs.version }}\n          commit: ${{ github.sha }}\n          body: 'Release ${{ steps.meta.outputs.version }}'\n          artifacts: 'dist/plugin_package.zip,dist/plugin_package.zip.sig'\n          immutableCreate: true\n"
  },
  {
    "path": ".github/workflows/validate.yml",
    "content": "name: Validate\n\non:\n  push:\n    branches:\n      - 'main'\n  workflow_call:\n\nconcurrency:\n  group: validate-${{ github.ref_name }}\n  cancel-in-progress: true\n\nenv:\n  CAIDO_NODE_VERSION: 20\n  CAIDO_PNPM_VERSION: 9\n\njobs:\n  typecheck:\n    name: 'Typecheck'\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.CAIDO_NODE_VERSION }}\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v3.0.0\n        with:\n          version: ${{ env.CAIDO_PNPM_VERSION }}\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Run typechecker\n        run: pnpm typecheck\n\n  lint:\n    name: 'Lint'\n    runs-on: ubuntu-latest\n    timeout-minutes: 10\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.CAIDO_NODE_VERSION }}\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4.1.0\n        with:\n          version: ${{ env.CAIDO_PNPM_VERSION }}\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Run linter\n        run: pnpm lint"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 [Author]\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# EvenBetter [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20me)](https://twitter.com/bebiksior) [![Static Badge](https://img.shields.io/badge/TODO%20List-00000?style=flat&color=%233251ed)](https://github.com/users/bebiksior/projects/2)\n\n`EvenBetter` is a frontend Caido plugin that makes the [Caido](https://github.com/caido) experience even better 😎\n\nHere's what **EvenBetter** implements:\n\n- **Quick Decode**: quickly decode and edit encoded values within the request body on the Replay page\n- **Font picker**: feature in the EvenBetter settings that allows you to change the font of the Caido UI.\n- **EvenBetter Settings**: customize and toggle EvenBetter features\n- **Scope Share**: export/import scope presets\n- **Send to Match & Replace**: custom right-click menu button that sends selected text to the Match & Replace page\n- ... more small tweaks that improve overall [Caido](https://github.com/caido) experience\n\n## Sponsors\nMaintenance of EvenBetter is possible thanks to the following sponsors:\n\n<!-- sponsors --><a href=\"https://github.com/CRITSoftware\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;CRITSoftware.png\" width=\"60px\" alt=\"User avatar: CRIT Software\" /></a><!-- sponsors -->\n\n## Installation [Recommended]\n\n1. Open Caido, navigate to the `Plugins` sidebar page and then to the `Community Store` tab\n2. Find `EvenBetter` and click `Install`\n3. Done! 🎉\n\n## Installation [without auto-updates]\n\n1. Go to the [EvenBetter Releases tab](https://github.com/bebiksior/EvenBetter/releases) and download the latest `plugin_package.zip` file\n2. In your Caido instance, navigate to the `Plugins` page, click `Install` and select the downloaded `plugin_package.zip` file\n3. Done! 🎉\n\n\n## Changelog v4.0.1\n\n- Fixed Ctrl + Z issue in QuickDecode\n\n## Changelog v4.0.0\n\n- Migrated frontend from React to Vue\n- Made some features more stable and reliable\n- Removed `hide-sidebar-groups` feature (might return at some point)\n- Fixed bugs affecting latest Caido version\n\n## Contribution\n\nFeel free to contribute! If you'd like to request a feature or report a bug, please [create a GitHub Issue](https://github.com/bebiksior/EvenBetter/issues/new).\n"
  },
  {
    "path": "caido.config.ts",
    "content": "import { defineConfig } from '@caido-community/dev';\nimport vue from '@vitejs/plugin-vue';\nimport tailwindcss from \"tailwindcss\";\nimport tailwindPrimeui from \"tailwindcss-primeui\";\nimport tailwindCaido from \"@caido/tailwindcss\";\nimport path from \"path\";\nimport prefixwrap from \"postcss-prefixwrap\";\n\nconst id = \"evenbetter\";\nexport default defineConfig({\n  id,\n  name: \"EvenBetter\",\n  description: \"Collection of tweaks and improvements for Caido\",\n  version: \"4.0.3\",\n  author: {\n    name: \"bebiks\",\n    email: \"lukasz@caido.io\",\n    url: \"https://github.com/caido-community/EvenBetter\",\n  },\n  plugins: [\n    {\n      kind: \"backend\",\n      id: \"evenbetter-backend\",\n      root: \"packages/backend\",\n    },\n    {\n      kind: 'frontend',\n      id: \"evenbetter-frontend\",\n      root: 'packages/frontend',\n      backend: {\n        id: \"evenbetter-backend\",\n      },\n      vite: {\n        plugins: [vue()],\n        build: {\n          rollupOptions: {\n            external: [\n              '@caido/frontend-sdk',\n              \"@codemirror/autocomplete\",\n              \"@codemirror/commands\",\n              \"@codemirror/language\",\n              \"@codemirror/lint\",\n              \"@codemirror/search\",\n              \"@codemirror/state\",\n              \"@codemirror/view\",\n              \"@lezer/common\",\n              \"@lezer/highlight\",\n              \"@lezer/lr\",\n              \"vue\",\n            ]\n          }\n        },\n        resolve: {\n          alias: [\n            {\n              find: \"@\",\n              replacement: path.resolve(__dirname, \"packages/frontend/src\"),\n            },\n          ],\n        },\n        css: {\n          postcss: {\n            plugins: [\n              // This plugin wraps the root element in a unique ID\n              // This is necessary to prevent styling conflicts between plugins\n              prefixwrap(`#plugin--${id}`),\n\n              tailwindcss({\n                corePlugins: {\n                  preflight: false,\n                },\n                content: [\n                  './packages/frontend/src/**/*.{vue,ts}',\n                  './node_modules/@caido/primevue/dist/primevue.mjs'\n                ],\n                // Check the [data-mode=\"dark\"] attribute on the <html> element to determine the mode\n                // This attribute is set in the Caido core application\n                darkMode: [\"selector\", '[data-mode=\"dark\"]'],\n                plugins: [\n\n                  // This plugin injects the necessary Tailwind classes for PrimeVue components\n                  tailwindPrimeui,\n\n                  // This plugin injects the necessary Tailwind classes for the Caido theme\n                  tailwindCaido,\n                ],\n              })\n            ]\n          }\n        }\n      }\n    }\n  ]\n});\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import { defaultConfig } from \"@caido/eslint-config\";\n\n/** @type {import('eslint').Linter.Config } */\nexport default [\n  ...defaultConfig(),\n]\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"evenbetter\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"typecheck\": \"pnpm -r typecheck\",\n    \"lint\": \"eslint ./packages/**/src --fix\",\n    \"build\": \"caido-dev build\",\n    \"watch\": \"caido-dev watch\"\n  },\n  \"devDependencies\": {\n    \"@caido-community/dev\": \"^0.1.3\",\n    \"@caido/eslint-config\": \"^0.6.0\",\n    \"@caido/tailwindcss\": \"0.0.1\",\n    \"@vitejs/plugin-vue\": \"5.2.1\",\n    \"postcss-prefixwrap\": \"1.51.0\",\n    \"tailwindcss\": \"3.4.13\",\n    \"tailwindcss-primeui\": \"0.6.1\",\n    \"typescript\": \"5.5.4\"\n  }\n}\n"
  },
  {
    "path": "packages/backend/package.json",
    "content": "{\n  \"name\": \"backend\",\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"types\": \"src/index.ts\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@caido/sdk-backend\": \"^0.50.2\",\n    \"shared\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/backend/src/api/flags.ts",
    "content": "import {\n  error,\n  type FeatureFlag,\n  type FeatureFlagTag,\n  ok,\n  type Result,\n} from \"shared\";\n\nimport { FeatureFlagsStore } from \"../stores/flags\";\nimport { type BackendSDK } from \"../types\";\n\nexport const getFlags = (\n  _: BackendSDK,\n  filters?: Partial<FeatureFlag>,\n): Result<FeatureFlag[]> => {\n  const flagsStore = FeatureFlagsStore.get();\n  const flags = flagsStore.getFlags();\n  return ok(\n    flags.filter((flag) => {\n      if (filters?.tag && flag.tag !== filters.tag) {\n        return false;\n      }\n      return true;\n    }),\n  );\n};\n\nexport const updateFlags = async (\n  _: BackendSDK,\n  flags: FeatureFlag[],\n): Promise<Result<void>> => {\n  const flagsStore = FeatureFlagsStore.get();\n  await flagsStore.setFlags(flags);\n  return ok(undefined);\n};\n\nexport const getFlag = (\n  _: BackendSDK,\n  tag: FeatureFlagTag,\n): Result<boolean> => {\n  const flagsStore = FeatureFlagsStore.get();\n  const flags = flagsStore.getFlags();\n\n  const flag = flags.find((f) => f.tag === tag);\n  if (!flag) {\n    return error(`Flag ${tag} not found`);\n  }\n\n  return ok(flag.enabled);\n};\n\nexport const setFlag = async (\n  _: BackendSDK,\n  tag: FeatureFlagTag,\n  value: boolean,\n): Promise<Result<void>> => {\n  const flagsStore = FeatureFlagsStore.get();\n  await flagsStore.setFlag(tag, value);\n  return ok(undefined);\n};\n"
  },
  {
    "path": "packages/backend/src/api/settings.ts",
    "content": "import {\n  ok,\n  type Result,\n  type SettingKey,\n  type Settings,\n  type SettingValue,\n} from \"shared\";\n\nimport { SettingsStore } from \"../stores/settings\";\nimport { type BackendSDK } from \"../types\";\n\nexport const getSettings = (): Result<Settings> => {\n  const settingsStore = SettingsStore.get();\n  return ok(settingsStore.getSettings());\n};\n\nexport const updateSetting = <K extends SettingKey>(\n  _: BackendSDK,\n  key: K,\n  value: SettingValue<K>,\n): Result<void> => {\n  const settingsStore = SettingsStore.get();\n  settingsStore.updateSetting(key, value);\n  return ok(undefined);\n};\n"
  },
  {
    "path": "packages/backend/src/features/backend-test/index.ts",
    "content": "import { type BackendSDK } from \"../../types\";\nimport { createFeature } from \"../manager\";\n\nexport const backendTest = createFeature(\"backend-test\", {\n  onFlagEnabled: (sdk: BackendSDK) => {\n    sdk.console.log(\"Backend test flag enabled\");\n  },\n  onFlagDisabled: (sdk: BackendSDK) => {\n    sdk.console.log(\"Backend test flag disabled\");\n  },\n});\n"
  },
  {
    "path": "packages/backend/src/features/index.ts",
    "content": "// This file is used to import all the backend features.\n\nimport \"./backend-test\";\n"
  },
  {
    "path": "packages/backend/src/features/manager.ts",
    "content": "import { type FeatureFlag, type FeatureFlagTag } from \"shared\";\n\nimport { type BackendSDK } from \"../types\";\n\n/**\n * Example flag:\n * export const backendTest = createFeature(\"backend-test\", {\n  onFlagEnabled: (sdk: CaidoBackendSDK) => {\n    console.log(\"Backend test flag enabled\");\n  },\n  onFlagDisabled: (sdk: CaidoBackendSDK) => {\n    console.log(\"Backend test flag disabled\");\n  },\n});\n *\n * This FeatureManager is responsible for managing the feature flags on the backend.\n * It handles only the flags with kind \"backend\".\n * It creates a map of feature tags and their functions for enable and disable.\n * FlagStore will call these functions when a flag is enabled or disabled.\n */\n\ntype FeatureHandlers = {\n  onFlagEnabled: (sdk: BackendSDK) => void;\n  onFlagDisabled: (sdk: BackendSDK) => void;\n};\n\nconst featureMap = new Map<FeatureFlagTag, FeatureHandlers>();\n\nexport function createFeature(tag: FeatureFlagTag, handlers: FeatureHandlers) {\n  featureMap.set(tag, handlers);\n  return { tag, ...handlers };\n}\n\nexport function backendHandleFlagToggle(\n  tag: FeatureFlagTag,\n  enabled: boolean,\n  sdk: BackendSDK,\n) {\n  const handlers = featureMap.get(tag);\n  if (handlers) {\n    if (enabled) {\n      handlers.onFlagEnabled(sdk);\n    } else {\n      handlers.onFlagDisabled(sdk);\n    }\n  } else {\n    sdk.console.warn(`No handlers for feature flag ${tag}`);\n  }\n}\n\nexport function initializeFeatures(flags: FeatureFlag[], sdk: BackendSDK) {\n  flags.forEach((flag) => {\n    if (flag.kind === \"backend\" && flag.enabled) {\n      const handlers = featureMap.get(flag.tag);\n      if (handlers) {\n        handlers.onFlagEnabled(sdk);\n      } else {\n        sdk.console.warn(`No handlers for feature flag ${flag.tag}`);\n      }\n    }\n  });\n}\n"
  },
  {
    "path": "packages/backend/src/index.ts",
    "content": "import { type DefineAPI, type SDK } from \"caido:plugin\";\n\nimport { getFlag, getFlags, setFlag, updateFlags } from \"./api/flags\";\nimport { getSettings, updateSetting } from \"./api/settings\";\nimport { FeatureFlagsStore } from \"./stores/flags\";\nimport { SettingsStore } from \"./stores/settings\";\nimport { type BackendEvents } from \"./types\";\n\nexport type { BackendEvents } from \"./types\";\n\nexport type API = DefineAPI<{\n  getFlags: typeof getFlags;\n  setFlag: typeof setFlag;\n  getFlag: typeof getFlag;\n  updateFlags: typeof updateFlags;\n  getSettings: typeof getSettings;\n  updateSetting: typeof updateSetting;\n}>;\n\nexport async function init(sdk: SDK<API, BackendEvents>) {\n  await SettingsStore.initialize(sdk);\n  await FeatureFlagsStore.initialize(sdk);\n\n  sdk.api.register(\"getFlags\", getFlags);\n  sdk.api.register(\"setFlag\", setFlag);\n  sdk.api.register(\"getFlag\", getFlag);\n  sdk.api.register(\"updateFlags\", updateFlags);\n  sdk.api.register(\"getSettings\", getSettings);\n  sdk.api.register(\"updateSetting\", updateSetting);\n\n  sdk.events.onProjectChange((projectId) => {\n    sdk.api.send(\"caido:project-change\", projectId);\n  });\n}\n"
  },
  {
    "path": "packages/backend/src/stores/flags.ts",
    "content": "import { readFile, writeFile } from \"fs/promises\";\nimport * as path from \"path\";\n\nimport { type FeatureFlag, type FeatureFlagTag } from \"shared\";\n\nimport {\n  backendHandleFlagToggle,\n  initializeFeatures as initializeBackendFeatures,\n} from \"../features/manager\";\nimport { type BackendSDK } from \"../types\";\nimport { exists, getFlagsPath } from \"../utils/files\";\n\ninterface StoredFlag {\n  tag: FeatureFlagTag;\n  enabled: boolean;\n}\n\nexport class FeatureFlagsStore {\n  private static instance: FeatureFlagsStore;\n  private flags: FeatureFlag[];\n  private sdk: BackendSDK;\n\n  private constructor(sdk: BackendSDK) {\n    this.sdk = sdk;\n    this.flags = [\n      // {\n      //   tag: \"backend-test\",\n      //   description: \"Test backend flag\",\n      //   enabled: true,\n      //   kind: \"backend\",\n      // },\n      {\n        tag: \"share-scope\",\n        description: \"Share scope context menu button\",\n        enabled: true,\n        kind: \"frontend\",\n        requiresReload: false,\n      },\n      {\n        tag: \"quick-decode\",\n        description: \"Decode & encode selection on the Replay page\",\n        enabled: true,\n        kind: \"frontend\",\n      },\n      {\n        tag: \"clear-all-findings\",\n        description: \"Adds a button to clear all findings\",\n        enabled: true,\n        kind: \"frontend\",\n      },\n      {\n        tag: \"share-replay-collections\",\n        description: \"Export & import replay collections\",\n        enabled: true,\n        kind: \"frontend\",\n      },\n\n      {\n        tag: \"exclude-host-path\",\n        description:\n          \"Exclude Host/Path context menu buttons on the HTTP History page\",\n        enabled: true,\n        kind: \"frontend\",\n        requiresReload: true,\n      },\n      {\n        tag: \"quick-mar\",\n        description: \"Quick Match and Replace context menu button\",\n        enabled: true,\n        kind: \"frontend\",\n        requiresReload: true,\n      },\n      {\n        tag: \"colorize-by-method\",\n        description:\n          \"Colorize session tabs by their HTTP methods in the Replay page\",\n        enabled: false,\n        requiresReload: true,\n        kind: \"frontend\",\n        knownIssues: [\n          \"It's a bit unstable, so it's disabled by default. Working on a fix. If you want to try it, you can enable it by setting the flag to true.\",\n        ],\n      },\n      {\n        tag: \"share-filters\",\n        description: \"Export & import filter presets\",\n        enabled: true,\n        kind: \"frontend\",\n      },\n      {\n        tag: \"common-filters\",\n        description:\n          \"Creates and automatically updates common filters you may want to use. 1hr, recent, 24hr, 6hr, 12hr\",\n        enabled: true,\n        kind: \"frontend\",\n        requiresReload: false,\n      },\n      {\n        tag: \"command-palette-workflows\",\n        description: \"Adds all your convert workflows to the command palette\",\n        enabled: true,\n        kind: \"frontend\",\n        requiresReload: true,\n      },\n    ];\n  }\n\n  static async initialize(sdk: BackendSDK): Promise<FeatureFlagsStore> {\n    this.instance = new FeatureFlagsStore(sdk);\n    await this.instance.readFlags();\n    initializeBackendFeatures(this.instance.flags, sdk);\n    return this.instance;\n  }\n\n  public async readFlags() {\n    const flagsPath = getFlagsPath(this.sdk);\n    const fileExists = await exists(flagsPath);\n\n    if (!fileExists) {\n      this.sdk.console.log(\"Flags file not found. Creating a new flags file.\");\n      const flagsPath = await this.saveFlagsToFile(this.flags);\n      this.sdk.console.log(\"Flags file created at \" + path.resolve(flagsPath));\n    }\n\n    try {\n      const storedFlags: StoredFlag[] = JSON.parse(\n        await readFile(flagsPath, \"utf-8\"),\n      );\n      storedFlags.forEach((storedFlag) => {\n        const flag = this.flags.find((f) => f.tag === storedFlag.tag);\n        if (flag) {\n          flag.enabled = storedFlag.enabled;\n        }\n      });\n    } catch (error) {\n      this.sdk.console.error(\n        \"Unexpected error reading flags: \" + String(error),\n      );\n    }\n  }\n\n  private async saveFlagsToFile(flags: FeatureFlag[]): Promise<string> {\n    const flagsPath = getFlagsPath(this.sdk);\n    const storedFlags: StoredFlag[] = flags.map((flag) => ({\n      tag: flag.tag,\n      enabled: flag.enabled,\n    }));\n    await writeFile(flagsPath, JSON.stringify(storedFlags, null, 2));\n    return flagsPath;\n  }\n\n  static get(): FeatureFlagsStore {\n    if (FeatureFlagsStore.instance === undefined) {\n      throw new Error(\"FeatureFlagsStore not initialized\");\n    }\n\n    return FeatureFlagsStore.instance;\n  }\n\n  getFlags(): FeatureFlag[] {\n    return this.flags;\n  }\n\n  async setFlags(flags: FeatureFlag[]): Promise<void> {\n    this.flags = flags;\n    await this.saveFlagsToFile(flags);\n  }\n\n  async setFlag(tag: FeatureFlagTag, enabled: boolean): Promise<void> {\n    const flag = this.flags.find((f) => f.tag === tag);\n    if (flag) {\n      flag.enabled = enabled;\n      this.handleFlagToggle(flag, enabled);\n    }\n    await this.saveFlagsToFile(this.flags);\n  }\n\n  /**\n   * If flag has kind of \"frontend\", it will be sent to the frontend via sdk.api.send(eventName, value)\n   * If flag has kind of \"backend\", it will be handled by the backend\n   */\n  private handleFlagToggle(flag: FeatureFlag, enabled: boolean): void {\n    switch (flag.kind) {\n      case \"frontend\":\n        this.sdk.api.send(\"flag:toggled\", flag.tag, enabled);\n        break;\n      case \"backend\":\n        backendHandleFlagToggle(flag.tag, enabled, this.sdk);\n        break;\n      default:\n        this.sdk.console.warn(`Unknown flag kind: ${flag.kind}`);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/backend/src/stores/settings.ts",
    "content": "import { readFile, writeFile } from \"fs/promises\";\nimport * as path from \"path\";\n\nimport {\n  DEFAULT_SETTINGS,\n  getFontUrl,\n  type SettingKey,\n  type Settings,\n  type SettingValue,\n} from \"shared\";\n\nimport { type BackendSDK } from \"../types\";\nimport { exists, getSettingsPath } from \"../utils/files\";\n\nexport class SettingsStore {\n  private static instance?: SettingsStore;\n  private settings: Settings;\n  private sdk: BackendSDK;\n\n  private constructor(sdk: BackendSDK) {\n    this.settings = { ...DEFAULT_SETTINGS };\n    this.sdk = sdk;\n  }\n\n  public async readSettings() {\n    const settingsPath = getSettingsPath(this.sdk);\n    const fileExists = await exists(settingsPath);\n\n    if (!fileExists) {\n      this.sdk.console.log(\n        \"Settings file not found. Creating a new settings file.\",\n      );\n      const newSettingsPath = await this.saveSettingsToFile(this.settings);\n      this.sdk.console.log(\n        \"Settings file created at \" + path.resolve(newSettingsPath),\n      );\n    }\n\n    try {\n      const fileContent = await readFile(settingsPath, \"utf-8\");\n      const _settings = JSON.parse(fileContent);\n      Object.assign(this.settings, _settings);\n    } catch (error) {\n      this.sdk.console.error(\n        \"Unexpected error reading settings: \" + String(error),\n      );\n    }\n  }\n\n  private async saveSettingsToFile(settings: Settings): Promise<string> {\n    const settingsPath = getSettingsPath(this.sdk);\n    await writeFile(settingsPath, JSON.stringify(settings, null, 2));\n    return settingsPath;\n  }\n\n  static async initialize(sdk: BackendSDK): Promise<SettingsStore> {\n    if (SettingsStore.instance) {\n      throw new Error(\"SettingsStore already initialized\");\n    }\n\n    SettingsStore.instance = new SettingsStore(sdk);\n    await this.instance?.readSettings();\n\n    return SettingsStore.instance;\n  }\n\n  static get(): SettingsStore {\n    if (!SettingsStore.instance) {\n      throw new Error(\"SettingsStore not initialized\");\n    }\n\n    return SettingsStore.instance;\n  }\n\n  getSettings(): Settings {\n    return this.settings;\n  }\n\n  updateSetting<K extends SettingKey>(key: K, value: SettingValue<K>): void {\n    this.settings[key] = value;\n    this.saveSettingsToFile(this.settings);\n\n    if (key === \"customFont\") {\n      this.sdk.api.send(\"font:load\", value, getFontUrl(value as string));\n    }\n  }\n}\n"
  },
  {
    "path": "packages/backend/src/types.ts",
    "content": "import { type DefineEvents, type SDK } from \"caido:plugin\";\nimport { type FeatureFlagTag } from \"shared\";\n\nexport type BackendEvents = DefineEvents<{\n  \"flag:toggled\": (tag: FeatureFlagTag, enabled: boolean) => void;\n  \"font:load\": (fontName: string, fontUrl: string) => void;\n  \"caido:project-change\": () => void;\n}>;\nexport type BackendSDK = SDK<never, BackendEvents>;\n"
  },
  {
    "path": "packages/backend/src/utils/files.ts",
    "content": "import { mkdir, stat } from \"fs/promises\";\nimport path from \"path\";\n\nimport { type BackendSDK } from \"../types\";\n\nexport async function ensureDir(\n  sdk: BackendSDK,\n  directory: string,\n): Promise<boolean> {\n  try {\n    const dir = path.join(sdk.meta.path(), directory);\n    await mkdir(dir, { recursive: true });\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nexport function getSettingsPath(sdk: BackendSDK): string {\n  return path.join(sdk.meta.path(), \"settings.json\");\n}\n\nexport function getFlagsPath(sdk: BackendSDK): string {\n  return path.join(sdk.meta.path(), \"flags.json\");\n}\n\nexport async function exists(f: string): Promise<boolean> {\n  try {\n    await stat(f);\n    return true;\n  } catch {\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/backend/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"types\": [\"@caido/sdk-backend\"]\n  },\n  \"include\": [\"./src/**/*.ts\"]\n}\n"
  },
  {
    "path": "packages/frontend/package.json",
    "content": "{\n  \"name\": \"frontend\",\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"vue-tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@caido/primevue\": \"0.2.1\",\n    \"@pinia/colada\": \"^0.17.1\",\n    \"pinia\": \"3.0.3\",\n    \"primevue\": \"4.1.0\",\n    \"vue\": \"3.5.18\"\n  },\n  \"devDependencies\": {\n    \"@caido/sdk-backend\": \"^0.50.2\",\n    \"@caido/sdk-frontend\": \"^0.50.2\",\n    \"@codemirror/view\": \"6.38.1\",\n    \"backend\": \"workspace:*\",\n    \"shared\": \"workspace:*\",\n    \"vue-tsc\": \"3.0.5\"\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/components/FlagsList/Container.vue",
    "content": "<script setup lang=\"ts\">\nimport Button from \"primevue/button\";\nimport Card from \"primevue/card\";\nimport Column from \"primevue/column\";\nimport DataTable from \"primevue/datatable\";\nimport Dialog from \"primevue/dialog\";\nimport ToggleSwitch from \"primevue/toggleswitch\";\n\nimport { useForm } from \"./useForm\";\n\nconst {\n  flags,\n  isLoading,\n  dialogOpen,\n  handleDialogClose,\n  handleFlagChange,\n  confirmFlagChange,\n  hasKnownIssues,\n} = useForm();\n</script>\n\n<template>\n  <Card\n    class=\"h-full\"\n    :pt=\"{\n      body: { class: 'h-full p-0' },\n      content: { class: 'h-full flex flex-col p-0' },\n    }\"\n  >\n    <template #content>\n      <div class=\"p-4 text-base font-bold\">Features</div>\n\n      <div class=\"overflow-y-auto\">\n        <DataTable\n          :value=\"flags || []\"\n          :loading=\"isLoading\"\n          striped-rows\n          size=\"small\"\n        >\n          <Column field=\"tag\" header=\"Name\">\n            <template #body=\"{ data }\">\n              <span>{{ data.tag }}</span>\n              <span\n                v-if=\"hasKnownIssues(data.knownIssues)\"\n                v-tooltip.top=\"\n                  'Known issues: \\n' + data.knownIssues?.join('\\n')\n                \"\n                class=\"ml-2\"\n              >\n                <i class=\"fas fa-info-circle text-surface-300\"></i>\n              </span>\n            </template>\n          </Column>\n          <Column field=\"description\" header=\"Description\">\n            <template #body=\"{ data }\">\n              <span class=\"text-surface-200\">{{ data.description }}</span>\n            </template>\n          </Column>\n          <Column field=\"kind\" header=\"Kind\" />\n          <Column header=\"Requires Refresh?\">\n            <template #body=\"{ data }\">\n              {{ data.requiresReload ? \"Yes\" : \"No\" }}\n            </template>\n          </Column>\n          <Column header=\"Enabled\">\n            <template #body=\"{ data }\">\n              <ToggleSwitch\n                :model-value=\"data.enabled\"\n                @update:model-value=\"() => handleFlagChange(data)\"\n              />\n            </template>\n          </Column>\n        </DataTable>\n\n        <Dialog\n          v-model:visible=\"dialogOpen\"\n          header=\"Confirm Flag Change\"\n          modal\n          :closable=\"false\"\n        >\n          <p>\n            Disabling this flag will require a page reload. Are you sure you\n            want to change it?\n          </p>\n          <template #footer>\n            <Button label=\"Cancel\" @click=\"handleDialogClose\" />\n            <Button label=\"Confirm\" @click=\"confirmFlagChange\" />\n          </template>\n        </Dialog>\n      </div>\n    </template>\n  </Card>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/FlagsList/index.ts",
    "content": "export { default as FlagsList } from \"./Container.vue\";\n"
  },
  {
    "path": "packages/frontend/src/components/FlagsList/useForm.ts",
    "content": "import { type FeatureFlag, type FeatureFlagTag } from \"shared\";\nimport { ref } from \"vue\";\n\nimport { useSDK } from \"@/plugins/sdk\";\nimport { useFlagsQuery, useSetFlag } from \"@/queries/flags\";\n\nexport const useForm = () => {\n  const sdk = useSDK();\n\n  const { data: flags, isLoading } = useFlagsQuery();\n  const { setFlag } = useSetFlag();\n\n  const dialogOpen = ref(false);\n  const dialogFlagTag = ref<FeatureFlagTag | undefined>(undefined);\n\n  const handleDialogOpen = (flagTag: FeatureFlagTag) => {\n    dialogOpen.value = true;\n    dialogFlagTag.value = flagTag;\n  };\n\n  const handleDialogClose = () => {\n    dialogOpen.value = false;\n    dialogFlagTag.value = undefined;\n  };\n\n  const handleFlagChange = async (flag: FeatureFlag) => {\n    if (flag.requiresReload === true && flag.enabled === true) {\n      handleDialogOpen(flag.tag);\n      return;\n    }\n\n    const newValue = !flag.enabled;\n    await setFlag({ flag: flag.tag, value: newValue });\n    const status = newValue ? \"enabled\" : \"disabled\";\n    sdk.window.showToast(`Feature ${flag.tag} has been ${status}`, {\n      variant: \"success\",\n    });\n  };\n\n  const confirmFlagChange = async () => {\n    if (dialogFlagTag.value !== undefined) {\n      await setFlag({ flag: dialogFlagTag.value, value: false });\n      window.location.reload();\n    }\n  };\n\n  const hasKnownIssues = (issues?: string[]) =>\n    issues !== undefined && issues.length > 0;\n\n  return {\n    flags,\n    isLoading,\n    dialogOpen,\n    dialogFlagTag,\n    handleDialogOpen,\n    handleDialogClose,\n    handleFlagChange,\n    confirmFlagChange,\n    hasKnownIssues,\n  };\n};\n"
  },
  {
    "path": "packages/frontend/src/components/Settings/Container.vue",
    "content": "<script setup lang=\"ts\">\nimport Avatar from \"primevue/avatar\";\nimport Button from \"primevue/button\";\nimport Card from \"primevue/card\";\nimport Divider from \"primevue/divider\";\nimport Select from \"primevue/select\";\n\nimport { useForm } from \"./useForm\";\n\nconst {\n  isLoading,\n  error,\n  isPending,\n  fontOptions,\n  localSettings,\n  hasChanges,\n  handleSave,\n  openLink,\n} = useForm();\n</script>\n\n<template>\n  <div class=\"h-full flex flex-col gap-1\">\n    <Card\n      :pt=\"{\n        body: { class: 'p-0' },\n        content: { class: 'flex flex-col' },\n      }\"\n    >\n      <template #content>\n        <div class=\"p-4\">\n          <div class=\"text-base font-bold\">Settings</div>\n\n          <div v-if=\"isLoading\" class=\"text-sm\">Loading...</div>\n          <div v-else-if=\"error\" class=\"text-sm text-red-400\">Error</div>\n          <div v-else class=\"flex flex-col gap-3\">\n            <div class=\"flex flex-col gap-2 mt-2\">\n              <label class=\"text-sm text-surface-200\" for=\"font-select\"\n                >Custom Font</label\n              >\n              <Select\n                v-model=\"localSettings.customFont\"\n                input-id=\"font-select\"\n                :options=\"fontOptions\"\n                :disabled=\"isPending\"\n                class=\"w-full\"\n              />\n            </div>\n\n            <Button\n              label=\"Save Changes\"\n              :disabled=\"!hasChanges || isPending\"\n              @click=\"handleSave\"\n            />\n          </div>\n        </div>\n      </template>\n    </Card>\n\n    <Card\n      class=\"flex-1\"\n      :pt=\"{\n        body: { class: 'h-full p-0' },\n        content: { class: 'h-full flex flex-col' },\n      }\"\n    >\n      <template #content>\n        <div class=\"p-4 overflow-y-auto\">\n          <div class=\"flex items-center justify-between mb-3\">\n            <div class=\"text-base font-bold text-primary\">About EvenBetter</div>\n            <Button\n              label=\"Star on GitHub\"\n              icon=\"fas fa-star\"\n              size=\"small\"\n              @click=\"openLink('https://github.com/bebiksior/evenbetter')\"\n            />\n          </div>\n\n          <div class=\"flex flex-col gap-2\">\n            <div class=\"text-sm\">\n              <b>EvenBetter</b> is a collection of tweaks to make Caido even\n              better. You can find the source code on\n              <a\n                class=\"underline\"\n                href=\"https://github.com/bebiksior/evenbetter\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                >GitHub</a\n              >.\n            </div>\n            <div class=\"text-sm\">\n              Feel free to contribute to the project :D You can also submit\n              feature requests and bugs via the GitHub issues page. I'm always\n              looking for new ideas and improvements!\n            </div>\n            <div class=\"text-sm\">\n              Thanks for using this plugin. I hope it makes your Caido\n              experience better and more efficient.\n            </div>\n          </div>\n\n          <Divider />\n\n          <div class=\"flex flex-col gap-2\">\n            <div class=\"text-xs text-surface-400\">\n              Your feedback and suggestions are always welcome. My X profile is\n              <a\n                class=\"underline\"\n                href=\"https://x.com/bebiksior\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                >bebiksior</a\n              >\n              and my discord handle is <b>bebiks</b>\n            </div>\n            <div class=\"flex items-center gap-2\">\n              <Avatar\n                image=\"https://avatars.githubusercontent.com/u/71410238?v=4&size=30\"\n                size=\"normal\"\n                shape=\"circle\"\n              />\n              <div class=\"text-xs\">\n                Made with ❤️ by\n                <a\n                  class=\"underline\"\n                  href=\"https://x.com/bebiksior\"\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  >bebiks</a\n                >\n              </div>\n            </div>\n          </div>\n        </div>\n      </template>\n    </Card>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/src/components/Settings/index.ts",
    "content": "export { default as Settings } from \"./Container.vue\";\n"
  },
  {
    "path": "packages/frontend/src/components/Settings/useForm.ts",
    "content": "import { computed, ref, watch } from \"vue\";\n\nimport { useSettingsQuery, useUpdateSetting } from \"@/queries/settings\";\n\nexport const useForm = () => {\n  const fontOptions = [\n    \"Default\",\n    \"JetBrains Mono\",\n    \"Fira Code\",\n    \"Roboto Mono\",\n    \"Inconsolata\",\n  ];\n\n  const { data, isLoading, error } = useSettingsQuery();\n  const { updateSetting, isPending } = useUpdateSetting();\n\n  const localSettings = ref({ customFont: \"\" });\n  const hasChanges = computed(() => {\n    return (\n      data.value !== undefined &&\n      localSettings.value.customFont !== data.value.customFont\n    );\n  });\n\n  watch(\n    () => data.value,\n    (v) => {\n      if (v !== undefined) {\n        localSettings.value = { customFont: v.customFont };\n      }\n    },\n    { immediate: true },\n  );\n\n  const handleSave = async () => {\n    if (localSettings.value.customFont !== undefined) {\n      await updateSetting({\n        key: \"customFont\",\n        value: localSettings.value.customFont,\n      });\n    }\n  };\n\n  const openLink = (href: string) => {\n    window.open(href, \"_blank\", \"noopener,noreferrer\");\n  };\n\n  return {\n    data,\n    isLoading,\n    error,\n    isPending,\n    fontOptions,\n    localSettings,\n    hasChanges,\n    handleSave,\n    openLink,\n  };\n};\n"
  },
  {
    "path": "packages/frontend/src/dom/index.ts",
    "content": "export const initDOMManager = () => {\n  patchHistoryMethod(\"pushState\");\n  patchHistoryMethod(\"replaceState\");\n\n  window.addEventListener(\"popstate\", notify);\n  window.addEventListener(\"hashchange\", notify);\n  window.addEventListener(\"locationchange\", notify);\n\n  // we need to wait for the Caido app to be fully loaded, this is obviously a hack while we wait for the APIs to be ready\n  setTimeout(() => {\n    let attempts = 0;\n    const maxAttempts = 10;\n\n    const checkInterval = setInterval(() => {\n      attempts++;\n\n      if (document.querySelector(\".c-topbar__environment\")) {\n        clearInterval(checkInterval);\n        for (const cb of subscribers) {\n          cb({ oldHash: lastHash, newHash: lastHash });\n        }\n        return;\n      }\n\n      if (attempts >= maxAttempts) {\n        clearInterval(checkInterval);\n        for (const cb of subscribers) {\n          cb({ oldHash: lastHash, newHash: lastHash });\n        }\n      }\n    }, 50);\n  }, 150);\n};\n\n// Temporary workaround for missing sdk.navigation.onPageChange\ntype LocationChange = {\n  oldHash: string;\n  newHash: string;\n};\n\nexport type Callback = (change: LocationChange) => void;\n\nconst subscribers = new Set<Callback>();\nlet lastHash = window.location.hash;\n\nfunction notify(): void {\n  const newHash = window.location.hash;\n  if (newHash === lastHash) return;\n\n  setTimeout(() => {\n    for (const cb of subscribers) {\n      try {\n        cb({ oldHash: lastHash, newHash });\n      } catch {\n        // ignore\n      }\n    }\n  }, 1);\n\n  lastHash = newHash;\n}\n\nconst patchHistoryMethod = (method: \"pushState\" | \"replaceState\"): void => {\n  const original = history[method];\n  history[method] = function (...args: Parameters<typeof original>) {\n    const result = original.apply(this, args);\n    window.dispatchEvent(new Event(\"locationchange\"));\n    return result;\n  };\n};\n\nexport function onLocationChange(cb: Callback): () => void {\n  subscribers.add(cb);\n  return () => {\n    subscribers.delete(cb);\n  };\n}\n"
  },
  {
    "path": "packages/frontend/src/features/clear-all-findings/index.ts",
    "content": "import { onLocationChange } from \"@/dom\";\nimport { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nlet eventCancelFunction: (() => void) | undefined;\nlet clearAllButton: HTMLElement | undefined;\n\nconst deleteAllFindings = (sdk: FrontendSDK) => {\n  eventCancelFunction = onLocationChange((data) => {\n    if (data.newHash !== \"#/findings\") return;\n\n    attachClearAllButton(sdk);\n  });\n};\n\nconst attachClearAllButton = (sdk: FrontendSDK) => {\n  if (document.querySelector(\"#clear-all-findings\")) return;\n\n  clearAllButton = sdk.ui.button({\n    label: \"Clear All\",\n    size: \"small\",\n    variant: \"primary\",\n    leadingIcon: \"fas fa-trash\",\n  });\n\n  clearAllButton.id = \"clear-all-findings\";\n  clearAllButton.addEventListener(\"click\", async () => {\n    try {\n      const allFindingIds: string[] = [];\n      let hasNextPage = true;\n      let cursor: string | undefined = undefined;\n      const batchSize = 1000;\n\n      while (hasNextPage) {\n        let response;\n\n        if (cursor !== undefined) {\n          response = await sdk.graphql.getFindingsAfter({\n            after: cursor,\n            first: batchSize,\n            filter: {},\n            order: { by: \"ID\", ordering: \"DESC\" },\n          });\n\n          const findings = response.findings.edges;\n          allFindingIds.push(...findings.map((finding) => finding.node.id));\n\n          hasNextPage = response.findings.pageInfo.hasNextPage;\n          cursor = response.findings.pageInfo.endCursor ?? undefined;\n        } else {\n          response = await sdk.graphql.getFindingsByOffset({\n            limit: batchSize,\n            offset: 0,\n            filter: {},\n            order: { by: \"ID\", ordering: \"DESC\" },\n          });\n\n          const findings = response.findingsByOffset.edges;\n          allFindingIds.push(...findings.map((finding) => finding.node.id));\n\n          hasNextPage = response.findingsByOffset.pageInfo.hasNextPage;\n          cursor = response.findingsByOffset.pageInfo.endCursor ?? undefined;\n        }\n      }\n\n      if (allFindingIds.length > 0) {\n        await sdk.graphql.deleteFindings({\n          input: {\n            ids: allFindingIds,\n          },\n        });\n      }\n    } catch (error) {\n      console.error(\"Error clearing all findings:\", error);\n    }\n  });\n\n  const cardHeader = document.querySelector(\n    \".c-finding-table .c-card__header\",\n  ) as HTMLElement;\n  if (cardHeader !== null) {\n    cardHeader.appendChild(clearAllButton);\n    cardHeader.style.display = \"flex\";\n    cardHeader.style.justifyContent = \"space-between\";\n    cardHeader.style.alignItems = \"center\";\n  }\n};\n\nfunction cleanup() {\n  if (clearAllButton) {\n    clearAllButton.remove();\n  }\n\n  if (eventCancelFunction) {\n    eventCancelFunction();\n    eventCancelFunction = undefined;\n  }\n}\n\nexport const clearAllFindings = createFeature(\"clear-all-findings\", {\n  onFlagEnabled: (sdk: FrontendSDK) => {\n    deleteAllFindings(sdk);\n  },\n  onFlagDisabled: (sdk: FrontendSDK) => {\n    cleanup();\n  },\n});\n"
  },
  {
    "path": "packages/frontend/src/features/colorize-by-method/index.ts",
    "content": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nimport \"./style.css\";\n\nimport { onLocationChange } from \"@/dom\";\n\nlet abortController: AbortController | undefined = undefined;\nlet observer: MutationObserver | undefined = undefined;\n\nfunction setTabMethodAttributes(sdk: FrontendSDK) {\n  const tabs = document.querySelectorAll(\n    \".c-tab-list__body .c-tab-list__tab [data-session-id]\",\n  );\n\n  const promises = Array.from(tabs).map(async (tab) => {\n    const sessionId = tab.getAttribute(\"data-session-id\");\n    if (sessionId === null) return;\n\n    const method = await getHTTPMethod(sessionId, sdk);\n    tab.setAttribute(\"http-method\", method);\n  });\n\n  Promise.all(promises).then(() => {\n    setTimeout(() => {\n      updateSelectedTabColor();\n    }, 100);\n  });\n}\n\nfunction updateCurrentTabHTTPMethod(newMethod: string) {\n  const element = document.querySelector(\n    \".c-tab-list__tab [data-is-selected=true][data-session-id]\",\n  );\n  if (!element) return;\n\n  element.setAttribute(\"http-method\", newMethod);\n}\n\nfunction updateSelectedTabColor() {\n  const httpMethod = document.querySelector(\n    \".c-lang-http-request__method\",\n  )?.textContent;\n  if (httpMethod === null || httpMethod === undefined) return;\n\n  updateCurrentTabHTTPMethod(httpMethod);\n}\n\nfunction liveUpdateHTTPMethod() {\n  const controller = new AbortController();\n  document.addEventListener(\n    \"keyup\",\n    () => {\n      if (location.hash !== \"#/replay\") return;\n      updateSelectedTabColor();\n    },\n    { signal: controller.signal },\n  );\n  return controller;\n}\n\ntype HTTPMethod =\n  | \"GET\"\n  | \"POST\"\n  | \"PUT\"\n  | \"DELETE\"\n  | \"OPTIONS\"\n  | \"PATCH\"\n  | \"HEAD\"\n  | \"TRACE\"\n  | \"CONNECT\"\n  | \"UNKNOWN\";\n\nasync function getHTTPMethod(\n  sessionId: string,\n  sdk: FrontendSDK,\n): Promise<HTTPMethod> {\n  const data = await sdk.graphql.replayEntry({ id: sessionId });\n  if (data.replayEntry?.raw === undefined) return \"UNKNOWN\";\n\n  const method = data.replayEntry.raw.split(\"\\n\")[0]?.split(\" \")[0];\n  return (method as HTTPMethod) || \"UNKNOWN\";\n}\n\nfunction handleTabListChanges(sdk: FrontendSDK) {\n  setTabMethodAttributes(sdk);\n\n  const observer = new MutationObserver(() => {\n    setTabMethodAttributes(sdk);\n  });\n\n  const tabList = document.querySelector(\".c-tab-list__body\");\n  if (tabList) {\n    observer.observe(tabList, {\n      childList: true,\n    });\n  }\n\n  return observer;\n}\n\nconst cleanup = () => {\n  if (observer) {\n    observer.disconnect();\n    observer = undefined;\n  }\n\n  if (abortController) {\n    abortController.abort();\n    abortController = undefined;\n  }\n};\n\nfunction setup(sdk: FrontendSDK) {\n  cleanup();\n\n  if (window.location.hash === \"#/replay\") {\n    observer = handleTabListChanges(sdk);\n  }\n}\n\nexport const colorizeByMethod = createFeature(\"colorize-by-method\", {\n  onFlagEnabled: (sdk) => {\n    setup(sdk);\n\n    setTimeout(() => {\n      abortController = liveUpdateHTTPMethod();\n    }, 2000);\n\n    onLocationChange((data) => {\n      cleanup();\n\n      if (data.newHash === \"#/replay\") {\n        observer = handleTabListChanges(sdk);\n      }\n    });\n\n    sdk.backend.onEvent(\"caido:project-change\", () => {\n      let attempts = 0;\n      const maxAttempts = 25;\n      const interval = setInterval(() => {\n        const tabList = document.querySelector(\".c-tab-list__body\");\n        if (tabList) {\n          setup(sdk);\n          clearInterval(interval);\n        }\n        attempts++;\n        if (attempts >= maxAttempts) {\n          clearInterval(interval);\n        }\n      }, 200);\n    });\n  },\n  onFlagDisabled: () => {\n    cleanup();\n  },\n});\n"
  },
  {
    "path": "packages/frontend/src/features/colorize-by-method/style.css",
    "content": "[http-method=\"GET\"] {\n  border-bottom: 2px solid #2196f3 !important;\n}\n\n[http-method=\"POST\"] {\n  border-bottom: 2px solid #4caf50 !important;\n}\n\n[http-method=\"PUT\"] {\n  border-bottom: 2px solid #ff9800 !important;\n}\n\n[http-method=\"PATCH\"] {\n  border-bottom: 2px solid #ffeb3b !important;\n}\n\n[http-method=\"DELETE\"] {\n  border-bottom: 2px solid #f44336 !important;\n}\n\n[http-method=\"HEAD\"] {\n  border-bottom: 2px solid #9c27b0 !important;\n}\n\n[http-method=\"UNKNOWN\"] {\n  border-bottom: 2px solid gray !important;\n}\n"
  },
  {
    "path": "packages/frontend/src/features/command-palette-workflows/index.ts",
    "content": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\ninterface Workflow {\n  id: string;\n  name: string;\n  kind: string;\n}\n\nlet registeredCommandIds: string[] = [];\n\nconst registerWorkflowCommand = (workflow: Workflow, sdk: FrontendSDK) => {\n  // Skip if not a convert workflow\n  if (workflow.kind !== \"Convert\") {\n    return;\n  }\n\n  const commandId = `evenbetter:workflow:${workflow.id}`;\n\n  // Check if command is already registered to avoid duplicates\n  if (registeredCommandIds.includes(commandId)) {\n    return;\n  }\n\n  try {\n    sdk.commands.register(commandId, {\n      name: `c ${workflow.name}`,\n      group: \"Convert Workflows\",\n      run: async () => {\n        try {\n          // Get the selected text from the active editor\n          const selectedText = sdk.window.getActiveEditor()?.getSelectedText();\n\n          if (selectedText === undefined) {\n            sdk.window.showToast(\"No text selected\", {\n              variant: \"warning\",\n            });\n            return;\n          }\n\n          // Run the convert workflow with the selected text\n          const result = await sdk.graphql.runConvertWorkflow({\n            id: workflow.id,\n            input: selectedText,\n          });\n\n          if (result.runConvertWorkflow.error) {\n            const errorMessage =\n              typeof result.runConvertWorkflow.error === \"string\"\n                ? result.runConvertWorkflow.error\n                : JSON.stringify(result.runConvertWorkflow.error);\n\n            sdk.window.showToast(`Workflow error: ${errorMessage}`, {\n              variant: \"error\",\n            });\n            return;\n          }\n\n          // Get the output\n          const output = result.runConvertWorkflow.output;\n          if (output !== undefined && output !== null) {\n            // Check if the active editor is read-only\n            const activeEditor = sdk.window.getActiveEditor();\n            if (!activeEditor) {\n              sdk.window.showToast(\"No active editor\", {\n                variant: \"error\",\n              });\n              return;\n            }\n            if (activeEditor.isReadOnly()) {\n              // Copy output to clipboard\n              navigator.clipboard\n                .writeText(output)\n                .then(() => {\n                  sdk.window.showToast(\n                    \"Copied: \" + output.substring(0, 30) + \"...\",\n                    {\n                      variant: \"info\",\n                      duration: 7000,\n                    },\n                  );\n                })\n                .catch(() => {\n                  sdk.window.showToast(\"Failed to copy output\", {\n                    variant: \"error\",\n                  });\n                });\n            } else {\n              // Replace the selected text with the workflow output for editable editors\n              activeEditor.replaceSelectedText(output);\n            }\n          }\n        } catch (error) {\n          console.error(\"Error running workflow:\", error);\n          sdk.window.showToast(\"Failed to run workflow\", {\n            variant: \"error\",\n          });\n        }\n      },\n    });\n\n    registeredCommandIds.push(commandId);\n    sdk.commandPalette.register(commandId);\n  } catch (error) {\n    console.error(\n      `Failed to register command for workflow ${workflow.name}:`,\n      error,\n    );\n  }\n};\n\nconst loadExistingWorkflows = (sdk: FrontendSDK) => {\n  try {\n    const workflows = sdk.workflows.getWorkflows();\n\n    // Register commands for each existing convert workflow\n    workflows.forEach((workflow) => {\n      registerWorkflowCommand(workflow, sdk);\n    });\n  } catch (error) {\n    console.error(\"Failed to load existing workflows:\", error);\n    sdk.window.showToast(\"[EvenBetter] Failed to load existing workflows\", {\n      variant: \"error\",\n    });\n  }\n};\n\nconst setupWorkflowListener = (sdk: FrontendSDK) => {\n  // Listen for new workflows being created\n  sdk.workflows.onCreatedWorkflow((workflow) => {\n    registerWorkflowCommand(workflow.workflow, sdk);\n  });\n};\n\nconst init = (sdk: FrontendSDK) => {\n  // Load existing workflows and register their commands\n  loadExistingWorkflows(sdk);\n\n  // Set up listener for new workflows\n  setupWorkflowListener(sdk);\n};\n\nconst cleanup = (sdk: FrontendSDK) => {\n  // Clear registered command IDs when feature is disabled\n  registeredCommandIds = [];\n  window.location.reload(); //This will reload the page and it wont trigger the onFlagEnabled again\n};\n\nexport const commandPaletteWorkflows = createFeature(\n  \"command-palette-workflows\",\n  {\n    onFlagEnabled: init,\n    onFlagDisabled: cleanup,\n  },\n);\n"
  },
  {
    "path": "packages/frontend/src/features/common-filters/index.ts",
    "content": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nlet intervalId: Timeout | undefined;\n\ninterface TimeFilter {\n  name: string;\n  minutes: number;\n}\n\nconst TIME_FILTERS: TimeFilter[] = [\n  { name: \"recent\", minutes: 5 },\n  { name: \"1hr\", minutes: 60 },\n  { name: \"6hr\", minutes: 360 },\n  { name: \"12hr\", minutes: 720 },\n  { name: \"24hr\", minutes: 1440 },\n];\n\nconst createTimeBasedFilterQuery = (minutes: number): string => {\n  const now = new Date();\n  const pastTime = new Date(now.getTime() - minutes * 60 * 1000);\n  const formattedDate =\n    pastTime.getFullYear() +\n    \"-\" +\n    String(pastTime.getMonth() + 1).padStart(2, \"0\") +\n    \"-\" +\n    String(pastTime.getDate()).padStart(2, \"0\") +\n    \" \" +\n    String(pastTime.getHours()).padStart(2, \"0\") +\n    \":\" +\n    String(pastTime.getMinutes()).padStart(2, \"0\") +\n    \":\" +\n    String(pastTime.getSeconds()).padStart(2, \"0\");\n  return `req.created_at.gt:\"${formattedDate}\"`;\n};\n\nconst maintainTimeFilters = async (sdk: FrontendSDK) => {\n  try {\n    // Get all existing filters\n    const existingFilters = sdk.filters.getAll();\n\n    for (const timeFilter of TIME_FILTERS) {\n      const filterQuery = createTimeBasedFilterQuery(timeFilter.minutes);\n\n      // Check if filter already exists\n      const existingFilter = existingFilters.find(\n        (filter) => filter.name === timeFilter.name,\n      );\n\n      if (existingFilter) {\n        // Update existing filter if the query has changed\n        if (existingFilter.query !== filterQuery) {\n          await sdk.filters.update(existingFilter.id, {\n            name: timeFilter.name,\n            alias: timeFilter.name,\n            query: filterQuery,\n          });\n        }\n      } else {\n        // Create new filter\n        await sdk.filters.create({\n          name: timeFilter.name,\n          alias: timeFilter.name,\n          query: filterQuery,\n        });\n      }\n    }\n  } catch (error) {\n    console.error(\"Error maintaining time filters:\", error);\n  }\n};\n\nconst startFilterMaintenance = (sdk: FrontendSDK) => {\n  // Run immediately\n  maintainTimeFilters(sdk);\n\n  // Then run every minute\n  intervalId = setInterval(() => {\n    maintainTimeFilters(sdk);\n  }, 60000); // 60,000ms = 1 minute\n};\n\nconst stopFilterMaintenance = async (sdk: FrontendSDK) => {\n  if (intervalId) {\n    clearInterval(intervalId);\n    intervalId = undefined;\n  }\n\n  try {\n    // Get all existing filters\n    const existingFilters = sdk.filters.getAll();\n\n    // Remove filters that match our time filter names\n    for (const timeFilter of TIME_FILTERS) {\n      const existingFilter = existingFilters.find(\n        (filter) => filter.name === timeFilter.name,\n      );\n\n      if (existingFilter) {\n        await sdk.filters.delete(existingFilter.id);\n      }\n    }\n  } catch (error) {\n    console.error(\"Error cleaning up time filters:\", error);\n    sdk.window.showToast(\"[EvenBetter] Error cleaning up time filters\", {\n      variant: \"error\",\n    });\n  }\n};\n\nexport default createFeature(\"common-filters\", {\n  onFlagEnabled: (sdk: FrontendSDK) => {\n    startFilterMaintenance(sdk);\n  },\n  onFlagDisabled: (sdk: FrontendSDK) => {\n    stopFilterMaintenance(sdk);\n  },\n});\n"
  },
  {
    "path": "packages/frontend/src/features/exclude-host-path/index.ts",
    "content": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nconst excludeHostPathFunctionality = (sdk: FrontendSDK) => {\n  sdk.commands.register(\"eb:excludehost\", {\n    name: \"Exclude Host\",\n    run: async () => {\n      const selectedRequest = await getSelectedRequest(sdk);\n      if (!selectedRequest) return;\n\n      const currentQuery = sdk.httpHistory.getQuery();\n      const newQuery = currentQuery\n        ? `${currentQuery} AND req.host.ne:\"${selectedRequest.host}\"`\n        : `req.host.ne:\"${selectedRequest.host}\"`;\n      sdk.httpHistory.setQuery(newQuery);\n    },\n  });\n\n  sdk.menu.registerItem({\n    type: \"RequestRow\",\n    commandId: \"eb:excludehost\",\n    leadingIcon: \"fa fa-ban\",\n  });\n\n  sdk.commands.register(\"eb:excludepath\", {\n    name: \"Exclude Path\",\n    run: async () => {\n      const selectedRequest = await getSelectedRequest(sdk);\n      if (!selectedRequest) return;\n\n      const currentQuery = sdk.httpHistory.getQuery();\n      const newQuery = currentQuery\n        ? `${currentQuery} AND req.path.ne:\"${selectedRequest.path}\"`\n        : `req.path.ne:\"${selectedRequest.path}\"`;\n      sdk.httpHistory.setQuery(newQuery);\n    },\n  });\n\n  sdk.menu.registerItem({\n    type: \"RequestRow\",\n    commandId: \"eb:excludepath\",\n    leadingIcon: \"fa fa-ban\",\n  });\n};\n\nconst getSelectedRequestID = () => {\n  return document\n    .querySelector(\"[data-request-id]\")\n    ?.getAttribute(\"data-request-id\");\n};\n\nconst getSelectedRequest = async (sdk: FrontendSDK) => {\n  const selectedRequestID = getSelectedRequestID();\n  if (selectedRequestID === null || selectedRequestID === undefined) return;\n\n  const request = await sdk.graphql.request({\n    id: selectedRequestID,\n  });\n\n  return request?.request;\n};\n\nexport const excludeHostPath = createFeature(\"exclude-host-path\", {\n  onFlagEnabled: (sdk: FrontendSDK) => {\n    excludeHostPathFunctionality(sdk);\n  },\n  onFlagDisabled: (sdk: FrontendSDK) => {\n    location.reload();\n  },\n});\n"
  },
  {
    "path": "packages/frontend/src/features/index.ts",
    "content": "// This file is used to import all the frontend features.\n\nimport \"./quick-decode\";\nimport \"./clear-all-findings\";\nimport \"./exclude-host-path\";\nimport \"./quick-mar\";\nimport \"./share-scope\";\nimport \"./share-replay-collections\";\nimport \"./colorize-by-method\";\nimport \"./share-filters\";\nimport \"./common-filters\";\nimport \"./command-palette-workflows\";\n"
  },
  {
    "path": "packages/frontend/src/features/manager.ts",
    "content": "// This is the features manager for the frontend.\n// It creates a map of feature tags and their functions for enable and disable.\n// FlagStore will call these functions when a flag is enabled or disabled.\n\nimport { type FeatureFlag, type FeatureFlagTag } from \"shared\";\n\nimport { type FrontendSDK } from \"../types\";\n\ntype FeatureHandlers = {\n  onFlagEnabled: (sdk: FrontendSDK) => void;\n  onFlagDisabled: (sdk: FrontendSDK) => void;\n};\n\nconst featureMap = new Map<FeatureFlagTag, FeatureHandlers>();\n\nexport function createFeature(tag: FeatureFlagTag, handlers: FeatureHandlers) {\n  featureMap.set(tag, handlers);\n  return { tag, ...handlers };\n}\n\nfunction handleFlagToggle(\n  tag: FeatureFlagTag,\n  enabled: boolean,\n  sdk: FrontendSDK,\n) {\n  const handlers = featureMap.get(tag);\n  if (handlers) {\n    if (enabled) {\n      handlers.onFlagEnabled(sdk);\n    } else {\n      handlers.onFlagDisabled(sdk);\n    }\n  } else {\n    console.warn(`No handlers for feature flag ${tag}`);\n  }\n}\n\nfunction initializeFeatures(flags: FeatureFlag[], sdk: FrontendSDK) {\n  flags.forEach((flag) => {\n    if (flag.kind === \"frontend\" && flag.enabled) {\n      const handlers = featureMap.get(flag.tag);\n      if (handlers) {\n        handlers.onFlagEnabled(sdk);\n      } else {\n        console.warn(`No handlers for feature flag ${flag.tag}`);\n      }\n    }\n  });\n}\n\nexport const initialize = async (sdk: FrontendSDK) => {\n  sdk.backend.onEvent(\"flag:toggled\", (tag, enabled) => {\n    handleFlagToggle(tag, enabled, sdk);\n  });\n\n  const flags = await sdk.backend.getFlags({ kind: \"frontend\" });\n  switch (flags.kind) {\n    case \"Success\":\n      initializeFeatures(flags.value, sdk);\n      break;\n    case \"Error\":\n      console.error(flags.error);\n      sdk.window.showToast(\"Error initializing features\", {\n        variant: \"error\",\n      });\n      break;\n  }\n};\n"
  },
  {
    "path": "packages/frontend/src/features/quick-decode/index.ts",
    "content": "import { type FrontendSDK } from \"@/types\";\nimport { createFeature } from \"@/features/manager\";\n\nimport \"./quick-decode.css\";\n\nimport { onLocationChange } from \"@/dom\";\n\ninterface CodeMirrorEditor {\n  state: {\n    readOnly: boolean;\n    doc: {\n      lineAt: (pos: number) => {\n        number: number;\n        from: number;\n        text: string;\n      };\n    };\n    selection: {\n      main: {\n        from: number;\n        to: number;\n        head: number;\n      };\n    };\n    sliceDoc: (from: number, to: number) => string;\n  };\n  contentDOM: HTMLElement;\n  dispatch: (changes: any) => void;\n}\n\ninterface Selection {\n  from: number;\n  to: number;\n  text: string;\n}\n\nfunction unicodeEncode(str: string): string {\n  return str\n    .split(\"\")\n    .map((char) => {\n      const unicode = char.charCodeAt(0).toString(16).padStart(4, \"0\");\n      return `\\\\u${unicode}`;\n    })\n    .join(\"\");\n}\n\ninterface HistoryEntry {\n  content: string;\n  selectionStart: number;\n  selectionEnd: number;\n}\n\nclass QuickDecode {\n  private HTMLElement!: HTMLDivElement;\n  private quickDecode!: HTMLDivElement;\n  private textArea!: HTMLTextAreaElement;\n  private encodeMethodSelect!: HTMLSelectElement;\n  private encodeMethod: string;\n  private activeEditor: CodeMirrorEditor | undefined = undefined;\n  private selectionInterval: Timeout | undefined;\n  private copyIconElement: HTMLElement | undefined;\n  private undoStack: HistoryEntry[] = [];\n  private redoStack: HistoryEntry[] = [];\n  private isUpdatingFromHistory: boolean = false;\n  private lastSavedContent: string = \"\";\n\n  constructor() {\n    this.initializeHTMLElement();\n    this.initializeResizer();\n    this.initializeSelectedTextDiv();\n    this.initializeTextArea();\n    this.initializeEncodingMethodSelect();\n    this.initializeCopyIcon();\n\n    this.encodeMethod = \"none\";\n    this.startMonitoringSelection();\n  }\n\n  private initializeHTMLElement(): void {\n    this.HTMLElement = document.createElement(\"div\");\n    this.HTMLElement.id = \"plugin--evenbetter\";\n\n    this.quickDecode = document.createElement(\"div\");\n    this.quickDecode.classList.add(\"evenbetter__qd-body\");\n    this.quickDecode.style.display = \"none\";\n\n    this.HTMLElement.appendChild(this.quickDecode);\n  }\n\n  private initializeResizer(): void {\n    const resizer = document.createElement(\"div\");\n    resizer.id = \"evenbetter__qd-resizer\";\n\n    let isResizing = false;\n    let startY: number;\n\n    const resize = (e: MouseEvent) => {\n      if (!isResizing) return;\n      const diffY = startY - e.clientY;\n      const newHeight = Math.max(10, this.quickDecode.offsetHeight + diffY);\n      this.quickDecode.style.height = `${newHeight}px`;\n      startY = e.clientY;\n    };\n\n    const stopResize = () => {\n      isResizing = false;\n      document.removeEventListener(\"mousemove\", resize);\n      document.removeEventListener(\"mouseup\", stopResize);\n    };\n\n    resizer.addEventListener(\"mousedown\", (e: MouseEvent) => {\n      isResizing = true;\n      startY = e.clientY;\n\n      document.addEventListener(\"mousemove\", resize);\n      document.addEventListener(\"mouseup\", stopResize);\n    });\n\n    this.quickDecode.appendChild(resizer);\n  }\n\n  private initializeSelectedTextDiv(): void {\n    const selectedTextDiv = document.createElement(\"div\");\n    selectedTextDiv.classList.add(\"evenbetter__qd-selected-text\");\n\n    const selectedTextTopDiv = document.createElement(\"div\");\n    selectedTextTopDiv.classList.add(\"evenbetter__qd-selected-text-top\");\n\n    selectedTextDiv.appendChild(selectedTextTopDiv);\n    this.quickDecode.appendChild(selectedTextDiv);\n  }\n\n  private initializeTextArea(): void {\n    this.textArea = document.createElement(\"textarea\");\n    this.textArea.classList.add(\"evenbetter__qd-selected-text-box\");\n    this.textArea.setAttribute(\"autocomplete\", \"off\");\n    this.textArea.setAttribute(\"autocorrect\", \"off\");\n    this.textArea.setAttribute(\"autocapitalize\", \"off\");\n    this.textArea.setAttribute(\"spellcheck\", \"false\");\n\n    this.textArea.addEventListener(\"input\", this.handleInput.bind(this));\n    this.textArea.addEventListener(\"keydown\", this.handleKeyDown.bind(this));\n\n    const selectedTextDiv = this.quickDecode.querySelector(\n      \".evenbetter__qd-selected-text\",\n    );\n    if (selectedTextDiv) {\n      selectedTextDiv.appendChild(this.textArea);\n    }\n  }\n\n  private initializeEncodingMethodSelect(): void {\n    this.encodeMethodSelect = document.createElement(\"select\");\n    this.encodeMethodSelect.classList.add(\n      \"evenbetter__qd-selected-text-top-select\",\n    );\n\n    const options = [\n      { value: \"none\", label: \"None\" },\n      { value: \"base64\", label: \"Base64\" },\n      { value: \"unicode\", label: \"Unicode\" },\n      { value: \"url\", label: \"URL\" },\n      { value: \"url+base64\", label: \"URL + Base64\" },\n      { value: \"base64+url\", label: \"Base64 + URL\" },\n    ];\n\n    options.forEach(({ value, label }) => {\n      const optionElement = document.createElement(\"option\");\n      optionElement.value = value;\n      optionElement.textContent = label;\n      this.encodeMethodSelect.appendChild(optionElement);\n    });\n\n    this.encodeMethodSelect.addEventListener(\"change\", (e) => {\n      const target = e.target as HTMLSelectElement;\n      this.encodeMethod = target.value;\n      this.handleInput();\n    });\n\n    const selectedTextTopDiv = this.quickDecode.querySelector(\n      \".evenbetter__qd-selected-text-top\",\n    );\n    if (selectedTextTopDiv) {\n      selectedTextTopDiv.appendChild(this.encodeMethodSelect);\n    }\n  }\n\n  private initializeCopyIcon(): void {\n    this.copyIconElement = document.createElement(\"i\");\n    this.copyIconElement.classList.add(\"c-icon\", \"fas\", \"fa-copy\");\n    this.copyIconElement.addEventListener(\n      \"click\",\n      this.copyToClipboard.bind(this),\n    );\n\n    const selectedTextTopDiv = this.quickDecode.querySelector(\n      \".evenbetter__qd-selected-text-top\",\n    );\n    if (selectedTextTopDiv) {\n      selectedTextTopDiv.appendChild(this.copyIconElement);\n    }\n  }\n\n  private copyToClipboard(): void {\n    const decodedText = this.textArea.value;\n    if (decodedText) {\n      navigator.clipboard.writeText(decodedText);\n    }\n  }\n\n  private handleKeyDown(e: KeyboardEvent): void {\n    const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n    const isUndo =\n      (isMac ? e.metaKey : e.ctrlKey) && e.key === \"z\" && !e.shiftKey;\n    const isRedo =\n      (isMac ? e.metaKey : e.ctrlKey) &&\n      (e.key === \"y\" || (e.key === \"z\" && e.shiftKey));\n\n    if (isUndo) {\n      e.preventDefault();\n      this.undo();\n    } else if (isRedo) {\n      e.preventDefault();\n      this.redo();\n    }\n  }\n\n  private saveHistory(): void {\n    if (this.isUpdatingFromHistory) return;\n\n    const currentContent = this.textArea.value;\n    if (currentContent === this.lastSavedContent) return;\n\n    const historyEntry: HistoryEntry = {\n      content: this.lastSavedContent,\n      selectionStart: this.textArea.selectionStart,\n      selectionEnd: this.textArea.selectionEnd,\n    };\n\n    this.undoStack.push(historyEntry);\n    if (this.undoStack.length > 100) {\n      this.undoStack.shift();\n    }\n    this.redoStack = [];\n    this.lastSavedContent = currentContent;\n  }\n\n  private undo(): void {\n    if (this.undoStack.length === 0) return;\n\n    const currentEntry: HistoryEntry = {\n      content: this.textArea.value,\n      selectionStart: this.textArea.selectionStart,\n      selectionEnd: this.textArea.selectionEnd,\n    };\n    this.redoStack.push(currentEntry);\n\n    const previousEntry = this.undoStack.pop();\n    if (previousEntry) {\n      this.isUpdatingFromHistory = true;\n      this.textArea.value = previousEntry.content;\n      this.textArea.setSelectionRange(\n        previousEntry.selectionStart,\n        previousEntry.selectionEnd,\n      );\n      this.lastSavedContent = previousEntry.content;\n      this.isUpdatingFromHistory = false;\n      this.handleInput();\n    }\n  }\n\n  private redo(): void {\n    if (this.redoStack.length === 0) return;\n\n    const currentEntry: HistoryEntry = {\n      content: this.textArea.value,\n      selectionStart: this.textArea.selectionStart,\n      selectionEnd: this.textArea.selectionEnd,\n    };\n    this.undoStack.push(currentEntry);\n\n    const nextEntry = this.redoStack.pop();\n    if (nextEntry) {\n      this.isUpdatingFromHistory = true;\n      this.textArea.value = nextEntry.content;\n      this.textArea.setSelectionRange(\n        nextEntry.selectionStart,\n        nextEntry.selectionEnd,\n      );\n      this.lastSavedContent = nextEntry.content;\n      this.isUpdatingFromHistory = false;\n      this.handleInput();\n    }\n  }\n\n  private handleInput(): void {\n    if (!this.isUpdatingFromHistory) {\n      this.saveHistory();\n    }\n\n    let newContent = this.textArea.value;\n    if (\n      newContent.length <= 0 ||\n      !this.activeEditor ||\n      this.activeEditor.state.readOnly\n    )\n      return;\n\n    newContent = this.encodeContent(newContent);\n\n    this.activeEditor.dispatch({\n      changes: [\n        {\n          from: this.activeEditor.state.selection.main.from,\n          to: this.activeEditor.state.selection.main.to,\n          insert: newContent,\n        },\n      ],\n    });\n  }\n\n  private encodeContent(content: string): string {\n    switch (this.encodeMethod) {\n      case \"base64\":\n        return btoa(content);\n      case \"unicode\":\n        return unicodeEncode(content);\n      case \"url\":\n        return encodeURIComponent(content);\n      case \"url+base64\":\n        return encodeURIComponent(btoa(content));\n      case \"base64+url\":\n        return btoa(encodeURIComponent(content));\n      default:\n        return content;\n    }\n  }\n\n  public updateText(text: string): void {\n    this.textArea.value = text;\n    this.lastSavedContent = text;\n    this.undoStack = [];\n    this.redoStack = [];\n  }\n\n  public updateEncodeMethod(encodeMethod?: string): void {\n    this.encodeMethod = encodeMethod || \"none\";\n    this.encodeMethodSelect.value = this.encodeMethod;\n  }\n\n  public show(): void {\n    this.quickDecode.style.display = \"flex\";\n  }\n\n  public hide(): void {\n    this.quickDecode.style.display = \"none\";\n  }\n\n  public getElement(): HTMLDivElement {\n    return this.HTMLElement;\n  }\n\n  private getActiveEditor(): CodeMirrorEditor | undefined {\n    const activeElement = document.activeElement;\n    if (!activeElement) return;\n\n    const cmContent = activeElement.closest(\".cm-content\");\n    if (!cmContent) return;\n\n    return (cmContent as any)?.cmView?.view as CodeMirrorEditor;\n  }\n\n  private getCurrentSelection(): Selection {\n    const activeEditor = this.getActiveEditor();\n    if (!activeEditor) {\n      return { from: 0, to: 0, text: \"\" };\n    }\n\n    const { from, to } = activeEditor.state.selection.main;\n    return {\n      from,\n      to,\n      text: activeEditor.state.sliceDoc(from, to),\n    };\n  }\n\n  private startMonitoringSelection(): void {\n    const INTERVAL_DELAY = 50;\n    let lastSelection = this.getCurrentSelection();\n\n    this.selectionInterval = setInterval(() => {\n      const newSelection = this.getCurrentSelection();\n\n      if (\n        newSelection.from !== lastSelection.from ||\n        newSelection.to !== lastSelection.to\n      ) {\n        lastSelection = newSelection;\n        this.onSelectionChange(newSelection);\n      }\n    }, INTERVAL_DELAY);\n  }\n\n  public stopMonitoringSelection(): void {\n    if (this.selectionInterval) {\n      clearInterval(this.selectionInterval);\n    }\n  }\n\n  private isMouseOver(element: HTMLElement): boolean {\n    if (!element) return false;\n    return Array.from(document.querySelectorAll(\":hover\")).includes(element);\n  }\n\n  private onSelectionChange(selection: Selection): void {\n    if (this.isMouseOver(this.HTMLElement)) return;\n\n    const contextMenu = document.querySelector(\".p-contextmenu\");\n    if (contextMenu && this.isMouseOver(contextMenu as HTMLElement)) return;\n\n    if (selection.text === \"\") {\n      this.hide();\n      return;\n    }\n\n    this.activeEditor = this.getActiveEditor();\n\n    this.setReadOnly(this.activeEditor?.state.readOnly ?? false);\n    this.showQuickDecode(selection.text);\n  }\n\n  private showQuickDecode(text: string): void {\n    const decoded = this.tryToDecode(text);\n    this.updateText(decoded.decodedContent);\n    this.updateEncodeMethod(decoded.encodeMethod);\n    this.show();\n  }\n\n  private setReadOnly(readOnly: boolean): void {\n    this.textArea.disabled = readOnly;\n    this.encodeMethodSelect.disabled = readOnly;\n    if (readOnly) {\n      this.encodeMethodSelect.value = \"none\";\n    }\n  }\n\n  private isUrlEncoded(str: string): boolean {\n    const urlRegex = /(%[0-9A-Fa-f]{2})+/g;\n    return urlRegex.test(str);\n  }\n\n  private base64Decode(input: string): {\n    encodeMethod: string;\n    decodedContent: string;\n  } {\n    const modifiedInput = input.padEnd(Math.ceil(input.length / 4) * 4, \"=\");\n    const base64Regex =\n      /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\n\n    if (base64Regex.test(modifiedInput)) {\n      try {\n        const decodedBase64 = atob(modifiedInput);\n        return { encodeMethod: \"base64\", decodedContent: decodedBase64 };\n      } catch (error) {\n        // If decoding fails, return the original input\n      }\n    }\n\n    return { encodeMethod: \"none\", decodedContent: input };\n  }\n\n  private tryToDecode(input: string): {\n    encodeMethod: string;\n    decodedContent: string;\n  } {\n    const base64Decoded = this.base64Decode(input);\n    if (base64Decoded.encodeMethod !== \"none\") {\n      if (this.isUrlEncoded(base64Decoded.decodedContent)) {\n        try {\n          const decodedUrl = decodeURIComponent(base64Decoded.decodedContent);\n          return { encodeMethod: \"base64+url\", decodedContent: decodedUrl };\n        } catch (error) {\n          return base64Decoded;\n        }\n      }\n      return base64Decoded;\n    }\n\n    const unicodeRegex = /\\\\u([0-9a-fA-F]{4})/g;\n    if (unicodeRegex.test(input)) {\n      try {\n        const decodedUnicode = input.replace(unicodeRegex, (_, code) =>\n          String.fromCharCode(parseInt(code, 16)),\n        );\n        return { encodeMethod: \"unicode\", decodedContent: decodedUnicode };\n      } catch (error) {\n        // If decoding fails, continue to the next decoding attempt\n      }\n    }\n\n    if (this.isUrlEncoded(input)) {\n      try {\n        const decodedUrl = decodeURIComponent(input);\n        const base64Decoded = this.base64Decode(decodedUrl);\n        if (base64Decoded.encodeMethod !== \"none\" && input.length > 8) {\n          return {\n            encodeMethod: \"url+base64\",\n            decodedContent: base64Decoded.decodedContent,\n          };\n        }\n        return { encodeMethod: \"url\", decodedContent: decodedUrl };\n      } catch (error) {\n        // If decoding fails, continue to the next decoding attempt\n      }\n    }\n\n    return { encodeMethod: \"none\", decodedContent: input };\n  }\n\n  public cleanup(): void {\n    this.stopMonitoringSelection();\n    this.textArea.removeEventListener(\"input\", this.handleInput);\n    this.textArea.removeEventListener(\"keydown\", this.handleKeyDown);\n    this.encodeMethodSelect.removeEventListener(\"change\", this.handleInput);\n    if (this.copyIconElement) {\n      this.copyIconElement.removeEventListener(\"click\", this.copyToClipboard);\n    }\n    this.HTMLElement.remove();\n  }\n}\n\nclass QuickDecodeManager {\n  private sdk: FrontendSDK;\n  private quickDecode: QuickDecode | undefined = undefined;\n  private cleanupListener: (() => void) | undefined = undefined;\n  private projectChangeListener: (() => Promise<void>) | undefined = undefined;\n  private pageOpenListener: ((newHash: string) => void) | undefined = undefined;\n  private isCleaned: boolean = false;\n\n  constructor(sdk: FrontendSDK) {\n    this.sdk = sdk;\n  }\n\n  private removeExistingQuickDecode(): void {\n    const existingElements = document.getElementsByClassName(\n      \"evenbetter__qd-body\",\n    );\n    Array.from(existingElements).forEach((element) => {\n      element.remove();\n    });\n  }\n\n  private attachQuickDecode(): void {\n    this.removeExistingQuickDecode();\n\n    const sessionListBody = document.querySelector(\".size-full.flex.flex-col .size-full.flex.flex-col\");\n    if (!sessionListBody) return;\n\n    this.quickDecode = new QuickDecode();\n    sessionListBody.appendChild(this.quickDecode.getElement());\n  }\n\n  public init(): void {\n    const MAX_ATTEMPTS = 80;\n    const INTERVAL_DELAY = 25;\n\n    const attach = (): void => {\n      if (this.isCleaned) return;\n\n      let attemptCount = 0;\n      const interval = setInterval(() => {\n        if (this.isCleaned) {\n          clearInterval(interval);\n          return;\n        }\n\n        attemptCount++;\n        if (attemptCount > MAX_ATTEMPTS) {\n          console.error(\"[EvenBetter QuickDecode] Could not find editors\");\n          clearInterval(interval);\n          return;\n        }\n\n        const editors = document.querySelectorAll(\".cm-editor .cm-content\");\n        if (!editors.length) return;\n\n        clearInterval(interval);\n        this.attachQuickDecode();\n      }, INTERVAL_DELAY);\n    };\n\n    this.pageOpenListener = (newHash: string) => {\n      if (this.isCleaned) return;\n\n      if (newHash === \"#/replay\") {\n        this.cleanup(false);\n        attach();\n      } else {\n        this.cleanup(false);\n      }\n    };\n\n    this.projectChangeListener = async () => {\n      if (this.isCleaned) return;\n\n      this.cleanup(false);\n      await new Promise((resolve) => setTimeout(resolve, 500));\n      if (window.location.hash === \"#/replay\") attach();\n    };\n\n    this.sdk.backend.onEvent(\n      \"caido:project-change\",\n      this.projectChangeListener,\n    );\n    this.cleanupListener = onLocationChange((data) => {\n      this.pageOpenListener?.(data.newHash);\n    });\n  }\n\n  public cleanup(fullCleanup: boolean = true): void {\n    if (this.quickDecode) {\n      this.quickDecode.cleanup();\n      this.quickDecode = undefined;\n    }\n\n    this.removeExistingQuickDecode();\n\n    if (fullCleanup) {\n      if (this.cleanupListener) {\n        this.cleanupListener();\n        this.cleanupListener = undefined;\n      }\n\n      if (this.projectChangeListener) {\n        this.projectChangeListener = undefined;\n      }\n\n      if (this.pageOpenListener) {\n        this.pageOpenListener = undefined;\n      }\n\n      this.isCleaned = true;\n    }\n  }\n}\n\nlet manager: QuickDecodeManager | undefined = undefined;\n\nexport const quickDecode = createFeature(\"quick-decode\", {\n  onFlagEnabled: (sdk: FrontendSDK) => {\n    if (!manager) {\n      manager = new QuickDecodeManager(sdk);\n      manager.init();\n    }\n  },\n  onFlagDisabled: (sdk: FrontendSDK) => {\n    if (manager) {\n      manager.cleanup();\n      manager = undefined;\n    }\n  },\n});\n"
  },
  {
    "path": "packages/frontend/src/features/quick-decode/quick-decode.css",
    "content": ".evenbetter__qd-body {\n  max-height: 40vh;\n  height: 160px;\n\n  background-color: var(--c-bg-default);\n  word-wrap: break-word;\n  flex-direction: column;\n  overflow: hidden;\n  position: relative;\n}\n\n#evenbetter__qd-resizer {\n  cursor: ns-resize;\n  position: absolute;\n  height: 10px;\n  width: 100%;\n}\n\n.evenbetter__qd-selected-text-box {\n  white-space: pre-wrap;\n  padding: 0.4em;\n  background: var(--c-bg-default);\n  flex: 1;\n  resize: none;\n  font-size: 14px;\n}\n\n.evenbetter__qd-selected-text-top {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 0.5em;\n}\n\n.evenbetter__qd-selected-text-top i {\n  cursor: pointer;\n  transition: 0.2s ease transform;\n}\n\n.evenbetter__qd-selected-text-top i:active {\n  transform: scale(0.9);\n}\n\n.evenbetter__qd-selected-text-top-select {\n  background-color: var(--c-bg-default);\n}\n\n.evenbetter__qd-selected-text {\n  padding: 0.6em;\n  background: var(--c-bg-subtle);\n  height: 100%;\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n}\n"
  },
  {
    "path": "packages/frontend/src/features/quick-mar/index.ts",
    "content": "import { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\n\nconst init = (sdk: FrontendSDK) => {\n  sdk.commands.register(\"evenbetter:quickmar\", {\n    name: \"Send to Match & Replace\",\n    run: (context) => {\n      if (\n        context.type === \"RequestContext\" ||\n        context.type === \"ResponseContext\"\n      ) {\n        const selection = context.selection;\n        if (selection === \"\") {\n          sdk.window.showToast(\"No selection\", {\n            variant: \"warning\",\n          });\n          return;\n        }\n\n        const type = context.type === \"RequestContext\" ? \"request\" : \"response\";\n        sendToMatchAndReplace(selection, sdk, type);\n      }\n    },\n  });\n\n  sdk.menu.registerItem({\n    commandId: \"evenbetter:quickmar\",\n    leadingIcon: \"fas fa-wrench\",\n    type: \"Request\",\n  });\n\n  sdk.menu.registerItem({\n    commandId: \"evenbetter:quickmar\",\n    leadingIcon: \"fas fa-wrench\",\n    type: \"Response\",\n  });\n};\n\nconst sendToMatchAndReplace = async (\n  selection: string,\n  sdk: FrontendSDK,\n  type: \"request\" | \"response\",\n) => {\n  if (!selection) return;\n\n  sdk.navigation.goTo(\"/tamper\");\n\n  const collections = sdk.matchReplace.getCollections();\n\n  let collectionID: string;\n  if (collections.length === 0) {\n    const newCollection = await sdk.matchReplace.createCollection({\n      name: \"EvenBetter Collection\",\n    });\n\n    collectionID = newCollection.id;\n  } else {\n    const firstCollection = collections[0];\n    if (!firstCollection) return;\n\n    collectionID = firstCollection.id;\n  }\n\n  let name = selection;\n  if (selection.length > 30) {\n    name = selection.substring(0, 30) + \"...\";\n  }\n\n  sdk.matchReplace\n    .createRule({\n      name,\n      query: \"\",\n      section: {\n        kind: type === \"request\" ? \"SectionRequestBody\" : \"SectionResponseBody\",\n        operation: {\n          kind: \"OperationBodyRaw\",\n          matcher: {\n            kind: \"MatcherRawValue\",\n            value: selection,\n          },\n          replacer: {\n            kind: \"ReplacerTerm\",\n            term: \"\",\n          },\n        },\n      },\n      collectionId: collectionID,\n    })\n    .catch((err) => {\n      console.error(err);\n      sdk.window.showToast(\"Error occured while creating M&R rule.\", {\n        variant: \"error\",\n      });\n    });\n};\n\nexport const quickMatchAndReplace = createFeature(\"quick-mar\", {\n  onFlagEnabled: (sdk: FrontendSDK) => {\n    init(sdk);\n  },\n  onFlagDisabled: (sdk: FrontendSDK) => {\n    location.reload();\n  },\n});\n"
  },
  {
    "path": "packages/frontend/src/features/share-filters/index.ts",
    "content": "import { onLocationChange } from \"@/dom\";\nimport { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\nimport { downloadFile, importFile } from \"@/utils/file-utils\";\n\nlet filterTabObserver: MutationObserver | undefined = undefined;\nlet cancelListener: () => void;\nlet filterButtons: HTMLElement[] = [];\n\nexport const shareFilters = createFeature(\"share-filters\", {\n  onFlagEnabled: (sdk: FrontendSDK) => {\n    cancelListener = onLocationChange((data) => {\n      cleanupFilterElements();\n\n      if (data.newHash === \"#/filter\") {\n        addImportButton(sdk);\n        observeFilterTab(sdk);\n      }\n    });\n  },\n  onFlagDisabled: () => {\n    cleanupFilterElements();\n    if (cancelListener) {\n      cancelListener();\n    }\n  },\n});\n\nconst cleanupFilterElements = () => {\n  if (filterTabObserver) {\n    filterTabObserver.disconnect();\n    filterTabObserver = undefined;\n  }\n\n  filterButtons.forEach((b) => b.remove());\n  filterButtons = [];\n};\n\nconst addImportButton = (sdk: FrontendSDK) => {\n  const topbarLeft = document.querySelector(\n    \".c-topbar .c-topbar__left\",\n  ) as HTMLElement;\n  if (topbarLeft === null || document.querySelector(\"#filter-presets-import\"))\n    return;\n\n  const importButton = sdk.ui.button({\n    label: \"Import\",\n    leadingIcon: \"fas fa-file-upload\",\n    variant: \"tertiary\",\n    size: \"small\",\n  });\n  importButton.id = \"filter-presets-import\";\n  importButton.addEventListener(\"click\", () => {\n    importFile(\".json\", (content: string) => {\n      try {\n        const data = JSON.parse(content);\n\n        sdk.graphql\n          .createFilterPreset({\n            input: {\n              alias: data.alias,\n              clause: data.clause,\n              name: data.name,\n            },\n          })\n          .then(() => {\n            sdk.window.showToast(\"Filter preset imported successfully\", {\n              duration: 3000,\n              variant: \"success\",\n            });\n          })\n          .catch((error) => {\n            sdk.window.showToast(\n              `Failed to import filter preset: ${error.message}`,\n              {\n                duration: 3000,\n                variant: \"error\",\n              },\n            );\n          });\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n\n        sdk.window.showToast(\n          `Failed to import filter preset: ${errorMessage}`,\n          {\n            duration: 3000,\n            variant: \"error\",\n          },\n        );\n      }\n    });\n  });\n\n  filterButtons.push(importButton);\n\n  topbarLeft.appendChild(importButton);\n};\n\nconst observeFilterTab = (sdk: FrontendSDK) => {\n  const formBody = document.querySelector(\".c-form-body__actions\");\n  if (formBody !== null) {\n    attachDownloadButton(sdk);\n  }\n\n  const filterContainer = document.querySelector(\".c-filter\");\n\n  if (filterTabObserver) {\n    filterTabObserver.disconnect();\n    filterTabObserver = undefined;\n  }\n\n  filterTabObserver = new MutationObserver((mutations) => {\n    if (mutations.every((m) => m.attributeName === \"style\")) return;\n\n    if (!document.querySelector(\"#filter-presets-download\")) {\n      attachDownloadButton(sdk);\n    }\n  });\n\n  if (formBody) {\n    filterTabObserver.observe(formBody, {\n      childList: true,\n      attributes: true,\n      subtree: true,\n    });\n  }\n\n  if (filterContainer) {\n    filterTabObserver.observe(filterContainer, {\n      childList: true,\n      attributes: true,\n      subtree: true,\n    });\n  }\n};\n\nconst attachDownloadButton = (sdk: FrontendSDK) => {\n  if (document.querySelector(\"#filter-presets-download\")) return;\n\n  const formActions = document.querySelector(\".c-form-body__actions\");\n  if (!formActions) return;\n\n  const downloadButton = sdk.ui.button({\n    label: \"Download\",\n    leadingIcon: \"fas fa-file-arrow-down\",\n    variant: \"tertiary\",\n    size: \"small\",\n  });\n  filterButtons.push(downloadButton);\n\n  downloadButton.id = \"filter-presets-download\";\n\n  const button = downloadButton.querySelector(\"button\");\n  if (!button) return;\n\n  button.addEventListener(\"click\", () => {\n    const id = getActiveFilterPreset();\n    if (id === null || id === undefined) {\n      sdk.window.showToast(\"No filter preset selected\", {\n        duration: 3000,\n        variant: \"error\",\n      });\n      return;\n    }\n\n    sdk.graphql\n      .filterPresets()\n      .then((response) => {\n        const presets = response.filterPresets;\n        const preset = presets.find((p) => p.id === id);\n\n        if (preset === undefined) {\n          sdk.window.showToast(\"Filter preset not found\", {\n            duration: 3000,\n            variant: \"error\",\n          });\n          return;\n        }\n\n        const presetData = {\n          id: preset.id,\n          alias: preset.alias,\n          name: preset.name,\n          clause: preset.clause,\n        };\n\n        downloadFile(`filter-${preset.alias}.json`, JSON.stringify(presetData));\n        sdk.window.showToast(\"Filter preset downloaded successfully\", {\n          duration: 3000,\n          variant: \"success\",\n        });\n      })\n      .catch((error) => {\n        sdk.window.showToast(\n          `Failed to download filter preset: ${error.message}`,\n          {\n            duration: 3000,\n            variant: \"error\",\n          },\n        );\n      });\n  });\n\n  formActions.appendChild(downloadButton);\n};\n\nconst getActiveFilterPreset = () => {\n  return document\n    .querySelector(`.c-preset[data-is-selected=\"true\"]`)\n    ?.getAttribute(\"data-preset-id\");\n};\n"
  },
  {
    "path": "packages/frontend/src/features/share-replay-collections/index.ts",
    "content": "import { type RequestRawInput } from \"@caido/sdk-frontend/src/types/__generated__/graphql-sdk\";\n\nimport { onLocationChange } from \"@/dom\";\nimport { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\nimport { downloadFile, importFile } from \"@/utils/file-utils\";\n\nconst shareReplayCollectionsElements: HTMLElement[] = [];\nlet mutationObserver: MutationObserver | undefined = undefined;\nconst cancelFunctions: (() => void)[] = [];\n\nexport const shareReplayCollections = createFeature(\n  \"share-replay-collections\",\n  {\n    onFlagEnabled: (sdk: FrontendSDK) => {\n      collectionsShare(sdk);\n    },\n    onFlagDisabled: (sdk: FrontendSDK) => {\n      shareReplayCollectionsElements.forEach((element) => {\n        element.remove();\n      });\n\n      cancelFunctions.forEach((cancelFunction) => cancelFunction());\n\n      if (mutationObserver) {\n        mutationObserver.disconnect();\n        mutationObserver = undefined;\n      }\n    },\n  },\n);\n\nconst collectionsShare = (sdk: FrontendSDK) => {\n  const { stop: stopProjectChange } = sdk.backend.onEvent(\n    \"caido:project-change\",\n    () => {\n      if (window.location.hash === \"#/replay\") {\n        attachImportButton(sdk);\n        attachExportButton(sdk);\n      }\n    },\n  );\n\n  const stopPageOpen = onLocationChange((data) => {\n    if (data.newHash === \"#/replay\") {\n      attachImportButton(sdk);\n      attachExportButton(sdk);\n\n      if (mutationObserver) mutationObserver.disconnect();\n\n      mutationObserver = new MutationObserver((mutations) => {\n        mutations.forEach((mutation) => {\n          if (mutation.addedNodes.length > 0) {\n            attachExportButton(sdk);\n          }\n        });\n      });\n\n      const tree = document.querySelector(\".c-session-list-body__tree .c-tree\");\n      if (!tree) return;\n\n      mutationObserver.observe(tree, {\n        childList: true,\n        subtree: true,\n      });\n    }\n  });\n\n  cancelFunctions.push(stopProjectChange, stopPageOpen);\n};\n\nconst getCollectionByID = async (collectionID: string, sdk: FrontendSDK) => {\n  return await sdk.graphql.replaySessionCollections().then((data) => {\n    const collections = data.replaySessionCollections.edges;\n\n    return collections.find(\n      (collection) => collection.node.id === collectionID,\n    );\n  });\n};\n\nconst createSession = async (\n  collectionID: string,\n  request: RequestRawInput,\n  sdk: FrontendSDK,\n) => {\n  return await sdk.graphql.createReplaySession({\n    input: {\n      collectionId: collectionID,\n      requestSource: {\n        raw: request,\n      },\n    },\n  });\n};\n\nconst createCollection = async (collectionName: string, sdk: FrontendSDK) => {\n  return await sdk.graphql.createReplaySessionCollection({\n    input: {\n      name: collectionName,\n    },\n  });\n};\n\nconst downloadCollection = async (collectionID: string, sdk: FrontendSDK) => {\n  const collection = await getCollectionByID(collectionID, sdk);\n  if (!collection) return new Error(\"Collection not found\");\n\n  const replayEntries = [];\n\n  const sessions = collection.node.sessions;\n  if (sessions && sessions.length > 0) {\n    for (const session of sessions) {\n      const entryID = session.activeEntry?.id;\n      if (!entryID) continue;\n\n      const replayEntry = await sdk.graphql.replayEntry({\n        id: entryID,\n      });\n\n      replayEntries.push({ ...replayEntry.replayEntry, name: session.name });\n    }\n  }\n\n  const collectionExport = {\n    name: collection.node.name,\n    replayEntries: replayEntries,\n  };\n\n  const collectionName = collection.node.name.replaceAll(\" \", \"_\");\n\n  downloadFile(\n    \"collection_\" + collectionName + \".json\",\n    JSON.stringify(collectionExport),\n  );\n\n  sdk.window.showToast(\"Collection downloaded successfully!\", {\n    duration: 3000,\n    variant: \"success\",\n  });\n};\n\nconst importCollection = async (collection: any, sdk: FrontendSDK) => {\n  const collectionName = collection.name;\n  const newCollection = await createCollection(collectionName, sdk);\n\n  const newCollectionID =\n    newCollection.createReplaySessionCollection.collection?.id;\n  if (!newCollectionID) return;\n\n  const replayEntries = collection.replayEntries;\n  if (replayEntries && replayEntries.length > 0) {\n    for (const replayEntry of replayEntries) {\n      const requestRawInput: RequestRawInput = {\n        connectionInfo: {\n          host: replayEntry.connection.host,\n          port: replayEntry.connection.port,\n          isTLS: replayEntry.connection.isTLS,\n        },\n        raw: replayEntry.raw,\n      };\n\n      const newSession = await createSession(\n        newCollectionID,\n        requestRawInput,\n        sdk,\n      );\n\n      const sesionID = newSession.createReplaySession.session?.id;\n      if (!sesionID) return;\n\n      await sdk.graphql.renameReplaySession({\n        id: sesionID,\n        name: replayEntry.name,\n      });\n    }\n  }\n\n  sdk.window.showToast(\"Collection imported successfully!\", {\n    duration: 3000,\n    variant: \"success\",\n  });\n\n  return newCollectionID;\n};\n\nconst attachImportButton = (sdk: FrontendSDK) => {\n  if (document.querySelector(\"#import-collection\")) return;\n\n  const topbarLeft = document.querySelector(\".c-topbar__left\");\n  if (!topbarLeft) return;\n\n  const importButton = sdk.ui.button({\n    label: \"Import Collection\",\n    variant: \"tertiary\",\n    size: \"small\",\n    leadingIcon: \"fas fa-file-import\",\n  });\n  shareReplayCollectionsElements.push(importButton);\n\n  importButton.id = \"import-collection\";\n\n  importButton.style.float = \"left\";\n  importButton.style.marginRight = \"1em\";\n  importButton.addEventListener(\"click\", async () => {\n    importFile(\".json\", async (content: string) => {\n      try {\n        const collection = JSON.parse(content);\n        await importCollection(collection, sdk);\n      } catch (error) {\n        console.error(\"Failed to import collection:\", error);\n        sdk.window.showToast(\"Failed to import collection\", {\n          duration: 3000,\n          variant: \"error\",\n        });\n      }\n    });\n  });\n\n  topbarLeft.prepend(importButton);\n};\n\nconst attachExportButton = (sdk: FrontendSDK) => {\n  const collections = document.querySelectorAll(\".c-tree-collection\");\n  if (!collections || collections.length === 0) return;\n\n  collections.forEach((collection) => {\n    if (collection.querySelector(\"#download-collection\")) return;\n\n    const actions = collection.querySelector(\".c-tree-collection__actions\");\n    if (!actions) return;\n\n    const newElement = actions.childNodes[0]?.cloneNode(true) as HTMLElement;\n    if (!newElement) return;\n\n    shareReplayCollectionsElements.push(newElement);\n\n    const icon = newElement.querySelector(\"i\");\n    if (!icon) return;\n\n    newElement.id = \"download-collection\";\n    icon.classList.value = \"c-icon fas fa-file-arrow-down\";\n    newElement.addEventListener(\"click\", async () => {\n      const collectionID = collection.getAttribute(\"data-collection-id\");\n      if (!collectionID) return;\n\n      const err = await downloadCollection(collectionID, sdk);\n      if (err) {\n        sdk.window.showToast(\"Failed to download collection: \" + err, {\n          duration: 3000,\n          variant: \"error\",\n        });\n      }\n    });\n\n    actions.prepend(newElement);\n  });\n};\n"
  },
  {
    "path": "packages/frontend/src/features/share-scope/index.ts",
    "content": "import { onLocationChange } from \"@/dom\";\nimport { createFeature } from \"@/features/manager\";\nimport { type FrontendSDK } from \"@/types\";\nimport { downloadFile, importFile } from \"@/utils/file-utils\";\n\nlet scopeTabObserver: MutationObserver | undefined = undefined;\nlet cancelListener: () => void;\nlet scopeButtons: HTMLElement[] = [];\nexport const shareScope = createFeature(\"share-scope\", {\n  onFlagEnabled: (sdk: FrontendSDK) => {\n    cancelListener = onLocationChange((data) => {\n      if (data.newHash === \"#/scope\") {\n        observeScopeTab(sdk);\n        attachDownloadButton(sdk);\n        addImportButton(sdk);\n      } else {\n        if (scopeTabObserver !== undefined) {\n          scopeTabObserver.disconnect();\n          scopeTabObserver = undefined;\n        }\n      }\n    });\n  },\n  onFlagDisabled: () => {\n    if (scopeTabObserver !== undefined) {\n      scopeTabObserver.disconnect();\n      scopeTabObserver = undefined;\n    }\n    if (cancelListener !== undefined) {\n      cancelListener();\n    }\n    scopeButtons.forEach((b) => b.remove());\n    scopeButtons = [];\n  },\n});\n\nconst addImportButton = (sdk: FrontendSDK) => {\n  const topbarLeft = document.querySelector(\n    \".c-topbar .c-topbar__left\",\n  ) as HTMLElement;\n  if (\n    topbarLeft === null ||\n    document.querySelector(\"#scope-presents-import\") !== null\n  )\n    return;\n\n  const importButton = sdk.ui.button({\n    label: \"Import\",\n    leadingIcon: \"fas fa-file-upload\",\n    variant: \"tertiary\",\n    size: \"small\",\n  });\n  importButton.id = \"scope-presents-import\";\n  importButton.addEventListener(\"click\", () => {\n    importFile(\".json\", (content: string) => {\n      const data = JSON.parse(content);\n\n      sdk.scopes.createScope({\n        name: data.name,\n        allowlist: data.allowlist,\n        denylist: data.denylist,\n      });\n    });\n  });\n\n  scopeButtons.push(importButton);\n\n  setTimeout(() => {\n    topbarLeft.appendChild(importButton);\n  }, 0);\n};\n\nconst observeScopeTab = (sdk: FrontendSDK) => {\n  const presetForm = document.querySelector(\n    \".c-preset-form-create\",\n  )?.parentElement;\n  if (presetForm === null || presetForm === undefined) return;\n\n  if (scopeTabObserver !== undefined) {\n    scopeTabObserver.disconnect();\n    scopeTabObserver = undefined;\n  }\n\n  scopeTabObserver = new MutationObserver((m) => {\n    if (\n      m.some(\n        (m) =>\n          m.attributeName === \"style\" ||\n          (m.target as HTMLElement).classList.contains(\n            \"c-preset-form-create__header\",\n          ),\n      )\n    )\n      return;\n\n    attachDownloadButton(sdk);\n  });\n\n  scopeTabObserver.observe(presetForm, {\n    childList: true,\n    attributes: true,\n    subtree: true,\n  });\n};\n\nconst attachDownloadButton = (sdk: FrontendSDK) => {\n  document.querySelector(\"#scope-presents-download\")?.remove();\n\n  const presetCreateHeader = document.querySelector(\n    \".c-preset-form-create__header\",\n  );\n\n  const downloadButton = sdk.ui.button({\n    label: \"Download\",\n    leadingIcon: \"fas fa-file-arrow-down\",\n    variant: \"tertiary\",\n    size: \"small\",\n  });\n  scopeButtons.push(downloadButton);\n\n  downloadButton.id = \"scope-presents-download\";\n\n  const button = downloadButton.querySelector(\"button\");\n  if (button === null) return;\n\n  button.addEventListener(\"click\", () => {\n    const id = getActiveScopePreset();\n    if (id === undefined) return;\n\n    const scopes = sdk.scopes.getScopes();\n    const scope = scopes.find((s) => s.id === id);\n    if (scope === undefined) return;\n\n    downloadFile(\"scope-\" + scope.name + \".json\", JSON.stringify(scope));\n    sdk.window.showToast(\"Scope preset downloaded successfully\", {\n      duration: 3000,\n      variant: \"success\",\n    });\n  });\n\n  presetCreateHeader?.appendChild(downloadButton);\n};\n\nconst getActiveScopePreset = () => {\n  return document\n    .querySelector(`.c-preset[data-is-selected=\"true\"]`)\n    ?.getAttribute(\"data-preset-id\");\n};\n"
  },
  {
    "path": "packages/frontend/src/fonts/index.ts",
    "content": "import { getFontUrl } from \"shared\";\n\nimport { type FrontendSDK } from \"@/types\";\n\nfunction loadFont(font: string, fontUrl: string) {\n  const customFontElement = document.getElementById(\"eb-custom-font\");\n  if (customFontElement) {\n    document.head.removeChild(customFontElement);\n  }\n\n  const customFontStyleElement = document.getElementById(\n    \"eb-custom-font-style\",\n  );\n  if (customFontStyleElement) {\n    document.head.removeChild(customFontStyleElement);\n  }\n\n  if (font === \"Default\") return;\n\n  const link = document.createElement(\"link\");\n  link.href = fontUrl;\n  link.rel = \"stylesheet\";\n  link.id = \"eb-custom-font\";\n\n  const style = document.createElement(\"style\");\n  style.id = \"eb-custom-font-style\";\n  style.textContent = `body { font-family: ${font} !important; }`;\n\n  document.head.appendChild(link);\n  document.head.appendChild(style);\n}\n\nexport async function initFontLoader(sdk: FrontendSDK) {\n  sdk.backend.onEvent(\"font:load\", loadFont);\n\n  const settings = await sdk.backend.getSettings();\n  if (settings.kind === \"Error\") return;\n\n  const customFont = settings.value.customFont;\n  if (customFont) {\n    loadFont(customFont, getFontUrl(customFont));\n  }\n}\n"
  },
  {
    "path": "packages/frontend/src/index.ts",
    "content": "import { Classic } from \"@caido/primevue\";\nimport { createPinia } from \"pinia\";\nimport PrimeVue from \"primevue/config\";\nimport Tooltip from \"primevue/tooltip\";\nimport { createApp } from \"vue\";\n\nimport \"./features\";\nimport { SDKPlugin } from \"./plugins/sdk\";\nimport \"./styles/index.css\";\nimport type { FrontendSDK } from \"./types\";\nimport App from \"./views/App.vue\";\n\nimport { initDOMManager } from \"@/dom\";\n\nimport { PiniaColada } from \"@pinia/colada\";\n\nimport { initialize } from \"@/features/manager\";\nimport { initFontLoader } from \"@/fonts\";\n\nexport const init = (sdk: FrontendSDK) => {\n  initDOMManager();\n  initialize(sdk);\n  initFontLoader(sdk);\n\n  const app = createApp(App);\n  const pinia = createPinia();\n\n  app.use(pinia);\n  app.use(PiniaColada);\n\n  app.use(PrimeVue, {\n    unstyled: true,\n    pt: Classic,\n  });\n  app.directive(\"tooltip\", Tooltip);\n\n  app.use(SDKPlugin, sdk);\n\n  const root = document.createElement(\"div\");\n  Object.assign(root.style, {\n    height: \"100%\",\n    width: \"100%\",\n  });\n\n  root.id = `plugin--evenbetter`;\n\n  app.mount(root);\n\n  sdk.navigation.addPage(\"/evenbetter\", {\n    body: root,\n  });\n\n  sdk.sidebar.registerItem(\"EvenBetter\", \"/evenbetter\", {\n    icon: \"fas fa-rocket\",\n  });\n};\n"
  },
  {
    "path": "packages/frontend/src/plugins/sdk.ts",
    "content": "import { inject, type InjectionKey, type Plugin } from \"vue\";\n\nimport { type FrontendSDK } from \"@/types\";\n\nconst KEY: InjectionKey<FrontendSDK> = Symbol(\"FrontendSDK\");\n\n// This is the plugin that will provide the FrontendSDK to VueJS\n// To access the frontend SDK from within a component, use the `useSDK` function.\nexport const SDKPlugin: Plugin = (app, sdk: FrontendSDK) => {\n  app.provide(KEY, sdk);\n};\n\n// This is the function that will be used to access the FrontendSDK from within a component.\nexport const useSDK = () => {\n  return inject(KEY) as FrontendSDK;\n};\n"
  },
  {
    "path": "packages/frontend/src/queries/flags.ts",
    "content": "import { useQuery } from \"@pinia/colada\";\nimport { type FeatureFlag, type FeatureFlagTag } from \"shared\";\nimport { computed, ref } from \"vue\";\n\nimport { useSDK } from \"@/plugins/sdk\";\n\nconst FLAGS_KEY = [\"flags\"] as const;\n\nexport const useFlagsQuery = () => {\n  const sdk = useSDK();\n\n  return useQuery<FeatureFlag[]>({\n    key: FLAGS_KEY,\n    query: async () => {\n      const res = await sdk.backend.getFlags();\n      if (res.kind === \"Error\") {\n        throw new Error(res.error);\n      }\n      return res.value;\n    },\n  });\n};\n\nexport const useSetFlag = () => {\n  const sdk = useSDK();\n  const { refetch } = useFlagsQuery();\n  const isPending = ref(false);\n\n  const setFlag = async (payload: { flag: FeatureFlagTag; value: boolean }) => {\n    isPending.value = true;\n    const res = await sdk.backend.setFlag(payload.flag, payload.value);\n    if (res.kind === \"Error\") {\n      isPending.value = false;\n      throw new Error(res.error);\n    }\n    await refetch();\n    isPending.value = false;\n  };\n\n  return {\n    setFlag,\n    isPending: computed(() => isPending.value),\n  };\n};\n"
  },
  {
    "path": "packages/frontend/src/queries/settings.ts",
    "content": "import { useQuery } from \"@pinia/colada\";\nimport {\n  type Result,\n  type SettingKey,\n  type Settings,\n  type SettingValue,\n} from \"shared\";\nimport { computed, ref } from \"vue\";\n\nimport { useSDK } from \"@/plugins/sdk\";\n\nconst SETTINGS_KEY = [\"settings\"] as const;\n\nexport const useSettingsQuery = () => {\n  const sdk = useSDK();\n\n  return useQuery<Settings>({\n    key: SETTINGS_KEY,\n    query: async () => {\n      const res = await sdk.backend.getSettings();\n      if (res.kind === \"Error\") {\n        throw new Error(res.error);\n      }\n      return res.value;\n    },\n  });\n};\n\nexport const useUpdateSetting = () => {\n  const sdk = useSDK();\n  const { refetch } = useSettingsQuery();\n  const isPending = ref(false);\n\n  const updateSetting = async <K extends SettingKey>(payload: {\n    key: K;\n    value: SettingValue<K>;\n  }) => {\n    isPending.value = true;\n    const res = await sdk.backend.updateSetting(payload.key, payload.value);\n    if (res.kind === \"Error\") {\n      isPending.value = false;\n      throw new Error(res.error);\n    }\n    await refetch();\n    isPending.value = false;\n  };\n\n  return {\n    updateSetting,\n    isPending: computed(() => isPending.value),\n  };\n};\n"
  },
  {
    "path": "packages/frontend/src/styles/index.css",
    "content": "@import \"tailwindcss/base\";\n@import \"tailwindcss/components\";\n@import \"tailwindcss/utilities\";\n\n* {\n  margin: 0;\n  padding: 0;\n  border: 0;\n  box-sizing: border-box;\n}\n"
  },
  {
    "path": "packages/frontend/src/types.ts",
    "content": "import { type Caido } from \"@caido/sdk-frontend\";\nimport { type API, type BackendEvents } from \"backend\";\n\nexport type FrontendSDK = Caido<API, BackendEvents>;\n"
  },
  {
    "path": "packages/frontend/src/utils/file-utils.ts",
    "content": "export const downloadFile = (name: string, content: string) => {\n  const a = document.createElement(\"a\");\n  a.href = URL.createObjectURL(\n    new Blob([content], { type: \"application/json\" }),\n  );\n  a.download = name;\n  document.body.appendChild(a);\n  a.click();\n  document.body.removeChild(a);\n};\n\nexport const importFile = (\n  accept: string,\n  onFileRead: (content: string) => void,\n) => {\n  const input = document.createElement(\"input\");\n  input.type = \"file\";\n  input.accept = accept;\n  input.style.display = \"none\";\n\n  input.addEventListener(\"change\", (event) => {\n    const target = event.target as HTMLInputElement;\n    if (!target.files || !target.files.length) return;\n\n    const file = target.files[0];\n    if (!file) return;\n\n    const reader = new FileReader();\n    reader.onload = (e) => {\n      const target = e.target as FileReader;\n      const content = target.result as string;\n      onFileRead(content);\n    };\n    reader.readAsText(file);\n  });\n\n  document.body.prepend(input);\n  input.click();\n  input.remove();\n};\n"
  },
  {
    "path": "packages/frontend/src/views/App.vue",
    "content": "<script setup lang=\"ts\">\nimport Splitter from \"primevue/splitter\";\nimport SplitterPanel from \"primevue/splitterpanel\";\n\nimport { FlagsList } from \"@/components/FlagsList\";\nimport { Settings } from \"@/components/Settings\";\n</script>\n\n<template>\n  <div class=\"h-full flex flex-col\">\n    <Splitter class=\"h-full\">\n      <SplitterPanel :size=\"40\" :min-size=\"20\">\n        <Settings />\n      </SplitterPanel>\n      <SplitterPanel :size=\"60\" :min-size=\"20\">\n        <FlagsList />\n      </SplitterPanel>\n    </Splitter>\n  </div>\n</template>\n"
  },
  {
    "path": "packages/frontend/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"ESNext\"],\n    \"types\": [\"@caido/sdk-backend\"],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    }\n  },\n  \"include\": [\"./src/**/*.ts\", \"./src/**/*.vue\"]\n}\n"
  },
  {
    "path": "packages/shared/package.json",
    "content": "{\n  \"name\": \"shared\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Shared types\",\n  \"author\": \"bebiks\",\n  \"license\": \"CC0-1.0\",\n  \"type\": \"module\",\n  \"types\": \"src/index.ts\",\n  \"main\": \"src/index.ts\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n  }\n}\n"
  },
  {
    "path": "packages/shared/src/flags.ts",
    "content": "export type FeatureFlagTag =\n  | \"exclude-host-path\"\n  | \"backend-test\"\n  | \"quick-decode\"\n  | \"clear-all-findings\"\n  | \"share-scope\"\n  | \"share-replay-collections\"\n  | \"colorize-by-method\"\n  | \"share-filters\"\n  | \"quick-mar\"\n  | \"common-filters\"\n  | \"command-palette-workflows\";\n\nexport type FeatureFlagKind = \"backend\" | \"frontend\";\n\nexport type FeatureFlag = {\n  tag: FeatureFlagTag;\n  description: string;\n  enabled: boolean;\n  kind: FeatureFlagKind;\n  knownIssues?: string[];\n  requiresReload?: boolean;\n};\n"
  },
  {
    "path": "packages/shared/src/fonts.ts",
    "content": "const fonts = [\n  { name: \"Default\", url: \"\" },\n  {\n    name: \"JetBrains Mono\",\n    url: \"https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap\",\n  },\n  {\n    name: \"Fira Code\",\n    url: \"https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap\",\n  },\n  {\n    name: \"Roboto Mono\",\n    url: \"https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap\",\n  },\n  {\n    name: \"Inconsolata\",\n    url: \"https://fonts.googleapis.com/css2?family=Inconsolata:wght@200..700&display=swap\",\n  },\n];\n\nexport function getFontUrl(font: string) {\n  const fontOption = fonts.find((f) => f.name === font);\n  return fontOption ? fontOption.url : \"\";\n}\n"
  },
  {
    "path": "packages/shared/src/index.ts",
    "content": "export * from \"./result\";\nexport * from \"./flags\";\nexport * from \"./settings\";\nexport * from \"./fonts\";\n"
  },
  {
    "path": "packages/shared/src/result.ts",
    "content": "export type Result<T> =\n  | { kind: \"Error\"; error: string }\n  | { kind: \"Success\"; value: T };\n\nexport function ok<T>(value: T): Result<T> {\n  return { kind: \"Success\", value };\n}\n\nexport function error<T>(error: string): Result<T> {\n  return { kind: \"Error\", error };\n}\n"
  },
  {
    "path": "packages/shared/src/settings.ts",
    "content": "export type Settings = {\n  customFont: string;\n};\n\nexport type SettingKey = keyof Settings;\nexport type SettingValue<K extends SettingKey> = Settings[K];\n\nexport const DEFAULT_SETTINGS: Settings = {\n  customFont: \"Default\",\n};\n"
  },
  {
    "path": "packages/shared/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"./src/**/*.ts\"]\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - packages/*\n\nonlyBuiltDependencies:\n  - esbuild\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"esnext\",\n    \"lib\": [\"ESNext\"],\n\n    \"jsx\": \"preserve\",\n    \"noImplicitAny\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"resolveJsonModule\": true,\n\n    \"moduleResolution\": \"bundler\",\n    \"esModuleInterop\": true,\n    \"sourceMap\": true,\n    \"noUnusedLocals\": true,\n\n    \"useDefineForClassFields\": true,\n    \"isolatedModules\": true,\n\n    \"baseUrl\": \".\"\n  }\n}\n"
  }
]