semrush-cli 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,221 +1,237 @@
1
1
  <p align="center">
2
- <img src="assets/logo.svg" width="220" alt="semrush-cli">
2
+ <img src="assets/logo-lockup.svg" width="720" alt="semrush cli logo">
3
3
  </p>
4
- <h1 align="center">semrush-cli</h1>
4
+
5
5
  <p align="center">
6
- <a href="https://github.com/Microck/semrush-cli/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Microck/semrush-cli/ci.yml?branch=main&style=flat-square&label=ci&color=000000" alt="CI"></a>
7
- <a href="https://www.npmjs.com/package/semrush-cli"><img src="https://img.shields.io/npm/v/semrush-cli?style=flat-square&color=000000" alt="npm"></a>
8
- <a href="https://github.com/Microck/semrush-cli/releases/latest"><img src="https://img.shields.io/github/v/release/Microck/semrush-cli?style=flat-square&color=000000" alt="release"></a>
9
- <a href="LICENSE"><img src="https://img.shields.io/github/license/Microck/semrush-cli?style=flat-square&color=000000" alt="license"></a>
6
+ <a href="https://github.com/Microck/semrush-cli/releases"><img src="https://img.shields.io/github/v/release/Microck/semrush-cli?display_name=tag&style=flat-square&label=release&color=000000" alt="release badge"></a>
7
+ <a href="https://www.npmjs.com/package/semrush-cli"><img src="https://img.shields.io/npm/dt/semrush-cli?style=flat-square&label=downloads&color=000000" alt="npm downloads"></a>
8
+ <a href="https://github.com/Microck/semrush-cli/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Microck/semrush-cli/ci.yml?branch=main&style=flat-square&label=ci&color=000000" alt="ci badge"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-mit-000000?style=flat-square" alt="license badge"></a>
10
10
  </p>
11
11
 
12
12
  ---
13
13
 
14
- semrush from your terminal and MCP clients. keyword research, organic rankings,
15
- competitors, backlinks and AI visibility, through your signed-in web account.
16
- built with TypeScript + Effect. sessions stored in Galleton.
14
+ `semrush` is a terminal cli for semrush. research keywords, check rankings and backlinks, compare competitors, manage projects and keyword lists, and run site audits from your own account. output is json, so you can pipe it into `jq` or give the same commands to an agent through mcp. built with typescript and effect.
15
+
16
+ [documentation](https://semrush.micr.dev) | [command reference](https://semrush.micr.dev/docs/reports) | [npm](https://www.npmjs.com/package/semrush-cli) | [mcp](https://semrush.micr.dev/docs/mcp)
17
17
 
18
- [docs](https://semrush.micr.dev) · [npm](https://www.npmjs.com/package/semrush-cli) · [mcp](#mcp) · [issues](https://github.com/Microck/semrush-cli/issues)
18
+ ![keyword research demo](assets/demo-keywords.gif)
19
19
 
20
20
  ## why
21
21
 
22
- 81 operations with the same input schemas in the CLI and MCP. JSON output for
23
- scripts, account totals and pagination, project management, and encrypted session storage.
24
- no Semrush developer API key or Python runtime required.
22
+ if you already pay for semrush, you should be able to use it from the terminal too. this cli uses your existing web session, so there is no developer api key to buy or configure. look up a keyword, save the result, compare a few domains, or let an agent do it through mcp.
25
23
 
26
- The executable is `semrush-cli`. It can coexist with other tools named `semrush`.
24
+ your plan's permissions and limits still apply. it doesn't cover the whole semrush suite. the [coverage page](https://semrush.micr.dev/docs/coverage) lists what works and what still needs the web ui.
27
25
 
28
26
  ## quickstart
29
27
 
30
- Requires Node.js 22+. Includes Galleton binaries for Linux x64/arm64,
31
- macOS x64/arm64 and Windows x64.
28
+ ### install
32
29
 
33
- ```sh
30
+ requires node.js 22 or newer. linux, macos and windows are supported.
31
+
32
+ ```bash
34
33
  npm install -g semrush-cli
35
- semrush-cli setup
36
- semrush-cli auth login
37
- semrush-cli domain-overview --domain openai.com --json
34
+ semrush setup
35
+ semrush auth login
36
+ semrush keyword-overview --keyword 'email marketing'
38
37
  ```
39
38
 
40
- `setup` installs Chromium and starts a private Galleton daemon. `auth login`
41
- opens an isolated browser for sign-in, verifies the session, and stores it in
42
- Galleton. It requires a graphical desktop. On Linux, missing Chromium system
43
- libraries can be installed with `npx patchright install-deps chromium`.
39
+ `semrush-cli` is the npm package. `semrush` is the command.
40
+
41
+ `setup` downloads chromium and starts the bundled galleton session engine. there is no separate python or go install. on linux, if chromium needs system libraries, run `npx patchright install-deps chromium`.
44
42
 
45
- To import an existing, authorized Chromium session on a headless machine:
43
+ ### auth
46
44
 
47
- ```sh
48
- semrush-cli auth import --cdp-url http://127.0.0.1:9234
49
- semrush-cli doctor --json
45
+ `semrush auth login` opens a browser. sign in to semrush there and the cli saves the session in galleton's encrypted vault. it doesn't ask for your password in the terminal.
46
+
47
+ ```bash
48
+ semrush auth login
49
+ semrush auth status --check --json
50
+ semrush doctor
50
51
  ```
51
52
 
52
- The browser must already expose CDP on a literal loopback HTTP address. This
53
- supports the browser managed by [semrush-web-mcp](https://github.com/Microck/semrush-web-mcp).
54
- Import does not stop or change that browser. Keep CDP private; forward it over
55
- SSH when necessary.
53
+ already have a signed-in browser with a local debugging port? import its session instead:
54
+
55
+ ```bash
56
+ semrush auth import --cdp-url http://127.0.0.1:9234
57
+ semrush doctor
58
+ ```
56
59
 
57
- ## projects
60
+ the browser has to be running with debugging enabled. import doesn't start or stop it. see the [authentication guide](https://semrush.micr.dev/docs/authentication) for headless hosts and remote browsers.
58
61
 
59
- ```sh
60
- semrush-cli projects --json
61
- semrush-cli project-create --name 'my website' --domain example.com --dry-run
62
- semrush-cli project-create --name 'my website' --domain example.com --confirm --json
63
- semrush-cli project-get --folder-id 123 --json
64
- semrush-cli project-update --folder-id 123 --name 'new name' --confirm
65
- semrush-cli project-set-tags --folder-id 123 --tags '["client"]' --confirm
66
- semrush-cli project-pin --folder-id 123 --pinned --confirm
67
- semrush-cli project-delete --folder-id 123 --confirm
62
+ semrush can expire your session. sign in or import again when that happens. the cli saves rotated cookies after successful requests, but doesn't renew expired sessions in the background.
63
+
64
+ ```bash
65
+ semrush auth logout
66
+ semrush daemon stop
67
+ semrush daemon start
68
68
  ```
69
69
 
70
- Project commands use the current web UI's folder IDs. Creation does not start
71
- crawls or share the project. Every write requires `--confirm`; `--dry-run` prints
72
- the request without authentication or network access. SEO project creation and
73
- automatic Site Audit/Position Tracking setup are separate, explicit commands.
74
- See [projects](https://semrush.micr.dev/docs/projects) and
75
- [web UI coverage](https://semrush.micr.dev/docs/coverage).
70
+ logout forgets this cli's saved session. it doesn't log out your other browsers. the daemon keeps running after the terminal closes. start it again after a reboot.
71
+
72
+ ## command surface
76
73
 
77
- ## keyword lists
74
+ 89 operations, available as cli commands and mcp tools.
78
75
 
79
- ```sh
80
- semrush-cli keyword-lists --json
81
- semrush-cli keyword-list-create --name 'research' --confirm --json
82
- semrush-cli keyword-list-import --basket-id 456 --owner-id 123 --keywords '["email marketing"]' --confirm
83
- semrush-cli keyword-list-keywords --basket-id 456 --owner-id 123 --page-size 20 --json
84
- semrush-cli keyword-list-add-tags --basket-id 456 --owner-id 123 --keywords '["email marketing"]' --tags '["opportunity"]' --confirm
85
- semrush-cli keyword-strategy-create --name 'example strategy' --domain example.com --dry-run
76
+ | command | what it does |
77
+ | --- | --- |
78
+ | `keyword-overview`, `keyword-magic` | volume, difficulty, cpc, intent, related keywords and filtered research |
79
+ | `keyword-serp`, `keyword-ads`, `keyword-clusters` | search results, ads and keyword clusters |
80
+ | `domain-overview`, `domain-keywords`, `domain-competitors` | organic rankings and competing domains |
81
+ | `domain-pages`, `domain-subdomains`, `domain-position-changes` | pages, subdomains and ranking changes |
82
+ | `keyword-gap`, `backlink-gap` | compare domains and find keyword or backlink opportunities |
83
+ | `backlink-links`, `backlink-referring-domains`, `backlink-anchors` | inspect backlinks, referring domains and anchor text |
84
+ | `domain-ai-visibility`, `domain-ai-countries`, `domain-ai-sources` | ai visibility, countries and citation sources |
85
+ | `project-create`, `project-get`, `project-update`, `project-delete` | create and manage projects |
86
+ | `project-tags`, `project-set-tags`, `project-pin` | organize your projects |
87
+ | `project-enable-seo`, `project-tools`, `project-toolkits` | enable seo and read project tool status |
88
+ | `keyword-lists`, `keyword-list-create`, `keyword-list-import` | manage keyword lists and import keywords |
89
+ | `keyword-strategy-create`, `keyword-list-cluster` | generate a strategy or cluster a list |
90
+ | `audit-create`, `audit-configure`, `audit-start` | configure and launch a site audit |
91
+ | `audit-status`, `audit-snapshots`, `audit-snapshot` | check progress and read audit results |
92
+ | `reports` | list every operation and its inputs |
93
+ | `mcp` | run the stdio mcp server |
94
+ | `doctor` | check the browser, session engine and semrush connection |
95
+
96
+ ```bash
97
+ semrush reports
98
+ semrush keyword-magic --help
99
+ semrush project-create --help
86
100
  ```
87
101
 
88
- Builder list IDs and owner IDs come from `keyword-lists`. Regular list creation
89
- and structured strategy generation are separate commands. Metric refresh and
90
- clustering require explicit confirmation and consume account allowances.
91
- See [keyword lists](https://semrush.micr.dev/docs/keyword-lists).
102
+ global flags: `--json`, `--no-color`, `--no-input`. reports return json with or without `--json`. objects and arrays use json arguments. use `--help` on a command for its filters, pagination and defaults.
92
103
 
93
- ## commands
104
+ ## examples
94
105
 
95
- ```sh
96
- semrush-cli keyword-overview --keyword 'email marketing' --json
97
- semrush-cli keyword-magic --keyword 'email marketing' --questions-only --page-size 20 --json
98
- semrush-cli domain-keywords --domain openai.com --page 2 --page-size 20 --json
99
- semrush-cli domain-competitors --domain openai.com --page-size 10 --json
100
- semrush-cli keyword-gap --domains '["openai.com","anthropic.com"]' --json
101
- semrush-cli backlink-gap --domains '["openai.com","anthropic.com"]' --json
102
- semrush-cli backlink-links --domain openai.com --filters '+|type||nofollow' --json
103
- semrush-cli domain-ai-visibility --domain openai.com --json
104
- semrush-cli snapshot-dates --database us --json
105
- semrush-cli reports --json
106
- semrush-cli domain-keywords --help
106
+ ### keyword research
107
+
108
+ ```bash
109
+ semrush keyword-overview --keyword 'email marketing'
110
+ semrush keyword-magic --keyword 'email marketing' --questions-only --page-size 20
111
+ semrush keyword-serp --keyword 'email marketing'
107
112
  ```
108
113
 
109
- | Area | Reports |
110
- | --- | --- |
111
- | Keywords | Overview, ideas, questions, SERPs, ads, clusters, paginated Keyword Magic and groups |
112
- | Organic | Rankings, pages, subdomains, changes, competitors, topics and trends |
113
- | Gaps | Keyword overlaps and gaps, backlink prospects and profile comparisons |
114
- | Backlinks | Links, referring domains, anchors, indexed pages, outbound domains, history and authority |
115
- | AI visibility | Visibility, mentions, citation sources and country distribution |
116
- | Keyword lists | Create, read, rename/delete, import, metrics, tags, deletion/undo, clustering and structured strategy generation |
117
- | Projects | Create, inspect, rename, delete, leave, tags, pinning, toolkit status, dashboard metrics and explicit SEO setup |
118
- | Account | Limits, permissions, databases, snapshots, folders, saved lists and audit campaigns |
119
-
120
- All successful reports use `{authenticated: true, report, data}`. Domain ranking
121
- and page reports include provider totals. Large integers remain exact as decimal
122
- strings. Errors go to stderr with a nonzero exit. `--json` works before or after
123
- the command. Objects and arrays use JSON flags; booleans support explicit `false`.
124
- See the [full command reference](https://semrush.micr.dev/docs/reports).
114
+ pull out the numbers you care about:
115
+
116
+ ```bash
117
+ semrush keyword-overview --keyword 'email marketing' \
118
+ | jq '.data | {phrase, volume, difficulty, cpc}'
119
+ ```
120
+
121
+ ### competitors and backlinks
122
+
123
+ ```bash
124
+ semrush domain-overview --domain openai.com
125
+ semrush domain-keywords --domain openai.com --page-size 20
126
+ semrush keyword-gap --domains '["openai.com","anthropic.com"]'
127
+ semrush backlink-links --domain openai.com
128
+ semrush backlink-gap --domains '["openai.com","anthropic.com"]'
129
+ ```
130
+
131
+ ### projects
132
+
133
+ preview a write before sending it:
134
+
135
+ ```bash
136
+ semrush project-create --name 'my website' --domain example.com --dry-run
137
+ ```
138
+
139
+ then create it when you're ready:
140
+
141
+ ```bash
142
+ semrush project-create --name 'my website' --domain example.com --confirm
143
+ ```
144
+
145
+ ![project request preview demo](assets/demo-projects.gif)
146
+
147
+ writes need `--confirm`. `--dry-run` validates the inputs and prints the request without connecting to semrush. creating a project doesn't start tools or share it.
148
+
149
+ ### keyword lists
150
+
151
+ ```bash
152
+ semrush keyword-lists
153
+ semrush keyword-list-create --name 'content ideas' --confirm
154
+ ```
155
+
156
+ use the list id and owner id returned by semrush:
157
+
158
+ ```bash
159
+ semrush keyword-list-import --basket-id 123 --owner-id 456 \
160
+ --keywords '["email marketing","email automation"]' --database us --confirm
161
+ semrush keyword-list-keywords --basket-id 123 --owner-id 456
162
+ ```
163
+
164
+ strategy generation, clustering and metric updates can consume your account allowance. they need explicit confirmation too. see [keyword lists](https://semrush.micr.dev/docs/keyword-lists).
165
+
166
+ ### site audits
167
+
168
+ ```bash
169
+ semrush project-enable-seo --folder-id 123 --confirm
170
+ ```
171
+
172
+ use the returned seo project `id` as the campaign id:
173
+
174
+ ```bash
175
+ semrush audit-create --campaign-id 456 --name 'my audit' \
176
+ --domain example.com --page-limit 100 --confirm
177
+ semrush audit-start --campaign-id 456 --confirm
178
+ semrush audit-status --campaign-id 456
179
+ semrush audit-snapshots --campaign-id 456
180
+ ```
181
+
182
+ creating an audit saves its settings. starting it is a separate command. defaults are manual scheduling and email off. see [site audit](https://semrush.micr.dev/docs/site-audit) for crawl settings and limits.
125
183
 
126
184
  ## mcp
127
185
 
128
- Complete setup and authentication, then configure your MCP client:
186
+ finish setup and sign in first, then add this to your client's mcp config:
129
187
 
130
188
  ```json
131
189
  {
132
190
  "mcpServers": {
133
191
  "semrush": {
134
- "command": "semrush-cli",
192
+ "command": "semrush",
135
193
  "args": ["mcp"]
136
194
  }
137
195
  }
138
196
  }
139
197
  ```
140
198
 
141
- Use an absolute executable path if your client does not inherit the terminal's
142
- PATH. `npx -y semrush-cli@0.3.0 mcp` also works. MCP tool names use underscores,
143
- for example `keyword_overview` and `domain_keywords`. Input schemas and reports
144
- match the CLI. The 62 read tools carry read-only annotations. The 19 write tools require
145
- `confirm:true`, or accept `dry_run:true` to preview the request. Delete and leave
146
- also carry destructive annotations. Login, arbitrary URLs and JavaScript
147
- execution are not MCP tools.
148
-
149
- ## sessions
150
-
151
- ```sh
152
- semrush-cli auth status --check --json
153
- semrush-cli auth logout
154
- semrush-cli daemon stop
155
- semrush-cli daemon start
156
- ```
199
+ the server exposes the same operations as the cli. tool names use underscores, such as `keyword_overview` and `project_create`. writes need `confirm: true`, or `dry_run: true` to preview the request. it doesn't expose login, arbitrary urls or javascript execution.
157
200
 
158
- Galleton stores credentials under `~/.local/share/semrush-cli/galleton`. The
159
- private daemon listens on `127.0.0.1:8768`. It continues after the terminal closes;
160
- run `daemon start` after reboot. The CLI installs no system startup service.
201
+ ## what it looks like
161
202
 
162
- Each CLI process restores its encrypted session into a fresh, transient Chromium
163
- context. The MCP server reuses one context and serializes requests. Successful
164
- reports capture cookie updates back to Galleton. Session expiry requires sign-in
165
- or import again. No anonymous fallback or automatic request retries.
203
+ the gifs use captured output for public keyword research and an offline project preview. no account details or credentials. the [capture notes and renderer](docs/demo/README.md) are in the repo if you want to regenerate them.
166
204
 
167
- The unchanged Galleton TypeScript SDK is vendored at a pinned upstream commit
168
- because it is not on npm. The bundled daemon adds a tested `static` provider for
169
- storage without renewal. Semrush has no proven renewal contract in the reference
170
- integration, so this tool does not invent one. See [architecture](docs/architecture.md)
171
- and [SDK provenance](src/vendor/README.md).
205
+ ## building from source
172
206
 
173
- | Variable | Default |
174
- | --- | --- |
175
- | `SEMRUSH_GALLETON_URL` | `http://127.0.0.1:8768` |
176
- | `SEMRUSH_GALLETON_TOKEN_FILE` | `~/.local/share/semrush-cli/galleton/api.token` |
177
- | `SEMRUSH_SESSION` | `semrush-cli` |
178
- | `SEMRUSH_BROWSER_EXECUTABLE` | Patchright's Chromium |
179
-
180
- The setup/daemon commands manage only the default CLI-owned daemon. An external
181
- daemon must support the documented static-provider extension. Never expose its
182
- API token or credential directory.
207
+ ```bash
208
+ git clone https://github.com/Microck/semrush-cli.git
209
+ cd semrush-cli
210
+ npm ci
211
+ npm run build
212
+ node dist/cli.js --help
213
+ npm run check
214
+ ```
183
215
 
184
- ## limits
216
+ the source build uses go to build the pinned galleton engine. npm releases include the engine for supported platforms.
185
217
 
186
- Semrush account permissions, quotas and product access apply. A read-only report
187
- can consume an allowance. Internal web endpoints may change. Provider caps remain
188
- visible; there is no unlimited-export claim. Galleton encrypts at rest but its key
189
- and vault share a private local directory. Other processes running as your OS user
190
- are within the same trust boundary.
218
+ ## documentation
191
219
 
192
- ## development
220
+ full docs live at [semrush.micr.dev](https://semrush.micr.dev). setup, authentication, projects, keyword lists, site audits, mcp and the complete command reference.
193
221
 
194
- ```sh
195
- npm ci
196
- npm run check
197
- npm --prefix docs-site ci
198
- npm --prefix docs-site run build
199
- ```
222
+ - [coverage](https://semrush.micr.dev/docs/coverage)
223
+ - [authentication](https://semrush.micr.dev/docs/authentication)
224
+ - [scripting](https://semrush.micr.dev/docs/scripting)
225
+ - [architecture](docs/architecture.md)
200
226
 
201
- Development builds require Go 1.23+ and `tar`/`patch` to build the pinned Galleton
202
- daemon with its storage-only extension. The build runs the upstream core suite
203
- with the extension's integration test. Published users do not need Go.
227
+ ## contributing
204
228
 
205
- Tests compare all 48 request shapes against the Python reference, use real
206
- Chromium and Galleton with a local HTTP fixture, and exercise the built executable.
207
- They never require a Semrush account. See [verification](docs/verification.md) for
208
- the separate live-account evidence.
229
+ issues and pull requests are welcome. if semrush changes an endpoint, include the command you ran and the error. leave cookies and account details out of the report.
209
230
 
210
- ## uninstall
231
+ ## disclaimer
211
232
 
212
- ```sh
213
- semrush-cli auth logout
214
- semrush-cli daemon stop
215
- npm uninstall -g semrush-cli
216
- ```
233
+ this is an unofficial client, not affiliated with semrush. it uses the web product's internal endpoints, which can change. your subscription and account limits apply.
217
234
 
218
- The private vault and browser cache remain on disk. Remove them only after
219
- checking they contain nothing you need.
235
+ ## license
220
236
 
221
- MIT. Independent project, not affiliated with Semrush.
237
+ [mit license](LICENSE)
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 230" role="img" aria-labelledby="title">
2
+ <title id="title">semrush cli</title>
3
+ <style>.wordmark{fill:#421983}@media(prefers-color-scheme:dark){.wordmark{fill:#fff}}</style>
4
+ <g transform="translate(12 38) scale(.48)"> <path d="M357.481 3C333.269 3 306.873 11.7835 277.382 27.1205L113.044 114.41H247.71L3 244.524H272.376L136.164 316.977H361.44C446.818 314.792 515.038 245.48 515.038 160.011C515.038 74.5425 444.178 3.04552 357.481 3.04552V3Z" fill="#ff642d"/>
5
+ <path d="M136.164 316.977L272.376 244.524H408L272 316.977Z" fill="#a76bff"/>
6
+ <path d="m317 116 47 44-47 44m73 0h52" fill="none" stroke="#421983" stroke-width="22" stroke-linecap="square" stroke-linejoin="miter"/>
7
+ </g>
8
+ <g class="wordmark" transform="translate(287 153) scale(0.13079394217530976 -0.13079394217530976)"><path d="M283 -12Q218 -12 163.0 12.5Q108 37 72.5 82.0Q37 127 30 188H149Q154 157 175.0 133.0Q196 109 225.5 96.0Q255 83 287 83Q331 83 358.5 103.0Q386 123 386 155Q386 186 367.5 203.0Q349 220 319.0 229.5Q289 239 253.5 246.0Q218 253 182.0 262.5Q146 272 116.0 289.5Q86 307 67.5 337.5Q49 368 49 417Q49 468 76.0 506.5Q103 545 152.0 566.5Q201 588 266 588Q326 588 377.5 565.5Q429 543 463.0 502.5Q497 462 504 408H383Q373 445 340.0 469.5Q307 494 261 494Q219 494 194.0 476.0Q169 458 169 429Q169 402 187.5 387.5Q206 373 236.0 364.5Q266 356 302.0 349.0Q338 342 374.0 331.5Q410 321 440.0 302.0Q470 283 488.5 250.5Q507 218 507 166Q507 114 478.0 73.5Q449 33 398.5 10.5Q348 -12 283 -12Z M867 -12Q783 -12 719.5 28.0Q656 68 621.0 136.0Q586 204 586 288Q586 373 620.0 440.5Q654 508 714.5 548.0Q775 588 856 588Q941 588 1002.0 547.5Q1063 507 1091.5 436.5Q1120 366 1107 276H705Q707 225 726.5 184.0Q746 143 782.0 119.5Q818 96 867 96Q920 96 954.5 121.5Q989 147 1000 188H1122Q1105 94 1035.0 41.0Q965 -12 867 -12ZM712 365H990Q986 418 950.5 451.5Q915 485 860 485Q807 485 767.0 453.5Q727 422 712 365Z M1945 0V350Q1945 409 1914.5 444.0Q1884 479 1831 479Q1774 479 1741.0 435.0Q1708 391 1708 327V0H1587V350Q1587 409 1556.5 444.0Q1526 479 1473 479Q1416 479 1383.5 435.0Q1351 391 1351 327V0H1229V576H1335L1345 482Q1370 533 1412.0 560.5Q1454 588 1508 588Q1571 588 1618.5 558.5Q1666 529 1689 476Q1719 533 1764.5 560.5Q1810 588 1866 588Q1955 588 2010.5 531.0Q2066 474 2066 375V0Z M2204 0V576H2310L2319 484Q2332 526 2363.5 551.0Q2395 576 2438 576H2529L2543 470H2446Q2385 470 2355.5 424.5Q2326 379 2326 302V0Z M2826 -12Q2765 -12 2718.0 14.0Q2671 40 2645.0 88.0Q2619 136 2619 201V576H2740V226Q2740 167 2773.0 132.5Q2806 98 2861 98Q2921 98 2955.0 141.5Q2989 185 2989 249V576H3110V0H3005L2995 95Q2969 44 2925.5 16.0Q2882 -12 2826 -12Z M3465 -12Q3400 -12 3345.0 12.5Q3290 37 3254.5 82.0Q3219 127 3212 188H3331Q3336 157 3357.0 133.0Q3378 109 3407.5 96.0Q3437 83 3469 83Q3513 83 3540.5 103.0Q3568 123 3568 155Q3568 186 3549.5 203.0Q3531 220 3501.0 229.5Q3471 239 3435.5 246.0Q3400 253 3364.0 262.5Q3328 272 3298.0 289.5Q3268 307 3249.5 337.5Q3231 368 3231 417Q3231 468 3258.0 506.5Q3285 545 3334.0 566.5Q3383 588 3448 588Q3508 588 3559.5 565.5Q3611 543 3645.0 502.5Q3679 462 3686 408H3565Q3555 445 3522.0 469.5Q3489 494 3443 494Q3401 494 3376.0 476.0Q3351 458 3351 429Q3351 402 3369.5 387.5Q3388 373 3418.0 364.5Q3448 356 3484.0 349.0Q3520 342 3556.0 331.5Q3592 321 3622.0 302.0Q3652 283 3670.5 250.5Q3689 218 3689 166Q3689 114 3660.0 73.5Q3631 33 3580.5 10.5Q3530 -12 3465 -12Z M3923 748V494Q3950 539 3991.5 563.5Q4033 588 4085 588Q4147 588 4194.0 562.0Q4241 536 4267.0 488.5Q4293 441 4293 375V0H4171V350Q4171 410 4138.0 444.0Q4105 478 4050 478Q3990 478 3956.5 434.5Q3923 391 3923 327V0H3801V748Z"/></g>
9
+ <text x="289" y="202" font-family="monospace" font-size="18" letter-spacing="3" fill="#a76bff">cli</text>
10
+ </svg>
package/assets/logo.png CHANGED
Binary file
package/assets/logo.svg CHANGED
@@ -1,10 +1,6 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 518 320" role="img" aria-labelledby="title">
2
- <title id="title">semrush-cli</title>
3
- <defs>
4
- <mask id="terminal">
5
- <rect width="518" height="320" fill="white"/>
6
- <path d="m340 130 29 29-29 29m45 0h35" fill="none" stroke="black" stroke-width="12" stroke-linejoin="miter"/>
7
- </mask>
8
- </defs>
9
- <path d="M357.481 3C333.269 3 306.873 11.7835 277.382 27.1205L113.044 114.41H247.71L3 244.524H272.376L136.164 316.977H361.44C446.818 314.792 515.038 245.48 515.038 160.011C515.038 74.5425 444.178 3.04552 357.481 3.04552V3Z" fill="#ff642d" mask="url(#terminal)"/>
2
+ <title id="title">semrush terminal comet</title>
3
+ <path d="M357.481 3C333.269 3 306.873 11.7835 277.382 27.1205L113.044 114.41H247.71L3 244.524H272.376L136.164 316.977H361.44C446.818 314.792 515.038 245.48 515.038 160.011C515.038 74.5425 444.178 3.04552 357.481 3.04552V3Z" fill="#ff642d"/>
4
+ <path d="M136.164 316.977L272.376 244.524H408L272 316.977Z" fill="#a76bff"/>
5
+ <path d="m317 116 47 44-47 44m73 0h52" fill="none" stroke="#421983" stroke-width="22" stroke-linecap="square" stroke-linejoin="miter"/>
10
6
  </svg>
@@ -1,7 +1,7 @@
1
- import { n as SessionError, r as UsageError } from "./errors-CU9T4acd.js";
2
- import { i as Sessions, n as origin } from "./session-layer-BDgTji_P.js";
1
+ import { n as SessionError, r as UsageError } from "./errors-DNn_XeoU.js";
2
+ import { a as Sessions, r as origin } from "./session-layer-Bq5OpnR5.js";
3
3
  import "./browser-service-BYYE-vSm.js";
4
- import { t as readBootstrap } from "./browser-layer-BJ4eQAUz.js";
4
+ import { t as readBootstrap } from "./browser-layer-gdm03TCV.js";
5
5
  import { Config, Effect, Option } from "effect";
6
6
  import { chromium } from "patchright";
7
7
 
@@ -79,4 +79,4 @@ const login = Effect.fn("Auth.login")(function* () {
79
79
 
80
80
  //#endregion
81
81
  export { importBrowser, login };
82
- //# sourceMappingURL=auth-BZhlPkLq.js.map
82
+ //# sourceMappingURL=auth-CrYOA8QT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-BZhlPkLq.js","names":["page","browser"],"sources":["../src/auth.ts"],"sourcesContent":["import { Config, Effect, Option } from 'effect';\nimport { chromium, type BrowserContext } from 'patchright';\nimport { Sessions } from './session-service.js';\nimport { origin } from './session-layer.js';\nimport { SessionError, UsageError } from './errors.js';\nimport { readBootstrap } from './browser-layer.js';\n\nconst authFailed = () => new SessionError({ message: 'Could not import a signed-in Semrush session. Check the browser, complete sign-in, and retry. No credentials were logged.' });\nconst authCall = <A>(run: () => Promise<A>) => Effect.tryPromise({ try: run, catch: authFailed });\n\nconst saveContext = Effect.fn('Auth.saveContext')(function* (context: BrowserContext) {\n const sessions = yield* Sessions;\n const page = yield* Effect.acquireRelease(authCall(() => context.newPage()), page => Effect.promise(() => page.close()));\n yield* authCall(() => page.goto(origin + '/robots.txt', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n const html = yield* authCall(() => page.evaluate(async () => {\n const response = await fetch('/analytics/overview/?db=us', { credentials: 'include', signal: AbortSignal.timeout(45000) });\n return response.ok ? response.text() : '';\n }));\n yield* Effect.try({ try: () => readBootstrap(html), catch: authFailed });\n const cookies = yield* authCall(() => context.cookies(origin));\n if (!cookies.length) return yield* Effect.fail(authFailed());\n // Bare cookies are intentionally bound to the single approved origin by Galleton.\n return yield* sessions.save({ provider: 'semrush', cookie_origin: origin, cookie_header: cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ') });\n});\n\nexport const importBrowser = Effect.fn('Auth.import')(function* (url: string) {\n const valid = yield* Effect.try({ try: () => {\n const parsed = new URL(url);\n if (parsed.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(parsed.hostname) || parsed.username || parsed.password) throw new Error('Not loopback');\n return parsed.origin;\n }, catch: () => new UsageError({ message: 'CDP must be a literal loopback HTTP URL. Forward remote browsers over SSH first.' }) });\n const browser = yield* authCall(() => chromium.connectOverCDP(valid, { timeout: 30000 }));\n // Closing a CDP connection disconnects this client and leaves the existing browser running.\n yield* Effect.addFinalizer(() => Effect.promise(() => browser.close()));\n const context = browser.contexts()[0];\n if (!context) return yield* Effect.fail(authFailed());\n const status = yield* saveContext(context);\n return { logged_in: true, source: 'galleton', session: status.id };\n});\n\nexport const login = Effect.fn('Auth.login')(function* () {\n const executable = yield* Config.option(Config.String('SEMRUSH_BROWSER_EXECUTABLE'));\n const browser = yield* Effect.acquireRelease(authCall(() => chromium.launch({ headless: false, executablePath: Option.getOrUndefined(executable) })), browser => Effect.promise(() => browser.close()));\n const context = yield* authCall(() => browser.newContext({ userAgent: 'OpenAI File Downloader, XaiImageApiFetch/1.0' }));\n const page = yield* authCall(() => context.newPage());\n yield* authCall(() => page.goto(origin + '/login/', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n yield* Effect.sync(() => process.stderr.write('Sign into Semrush in the browser. Waiting up to 5 minutes.\\n'));\n yield* authCall(() => page.waitForURL(url => url.hostname === 'www.semrush.com' && !url.pathname.startsWith('/login') && !url.pathname.startsWith('/signup'), { timeout: 300000 }));\n const status = yield* saveContext(context);\n return { logged_in: true, source: 'galleton', session: status.id };\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth-CrYOA8QT.js","names":["page","browser"],"sources":["../src/auth.ts"],"sourcesContent":["import { Config, Effect, Option } from 'effect';\nimport { chromium, type BrowserContext } from 'patchright';\nimport { Sessions } from './session-service.js';\nimport { origin } from './session-layer.js';\nimport { SessionError, UsageError } from './errors.js';\nimport { readBootstrap } from './browser-layer.js';\n\nconst authFailed = () => new SessionError({ message: 'Could not import a signed-in Semrush session. Check the browser, complete sign-in, and retry. No credentials were logged.' });\nconst authCall = <A>(run: () => Promise<A>) => Effect.tryPromise({ try: run, catch: authFailed });\n\nconst saveContext = Effect.fn('Auth.saveContext')(function* (context: BrowserContext) {\n const sessions = yield* Sessions;\n const page = yield* Effect.acquireRelease(authCall(() => context.newPage()), page => Effect.promise(() => page.close()));\n yield* authCall(() => page.goto(origin + '/robots.txt', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n const html = yield* authCall(() => page.evaluate(async () => {\n const response = await fetch('/analytics/overview/?db=us', { credentials: 'include', signal: AbortSignal.timeout(45000) });\n return response.ok ? response.text() : '';\n }));\n yield* Effect.try({ try: () => readBootstrap(html), catch: authFailed });\n const cookies = yield* authCall(() => context.cookies(origin));\n if (!cookies.length) return yield* Effect.fail(authFailed());\n // Bare cookies are intentionally bound to the single approved origin by Galleton.\n return yield* sessions.save({ provider: 'semrush', cookie_origin: origin, cookie_header: cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ') });\n});\n\nexport const importBrowser = Effect.fn('Auth.import')(function* (url: string) {\n const valid = yield* Effect.try({ try: () => {\n const parsed = new URL(url);\n if (parsed.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(parsed.hostname) || parsed.username || parsed.password) throw new Error('Not loopback');\n return parsed.origin;\n }, catch: () => new UsageError({ message: 'CDP must be a literal loopback HTTP URL. Forward remote browsers over SSH first.' }) });\n const browser = yield* authCall(() => chromium.connectOverCDP(valid, { timeout: 30000 }));\n // Closing a CDP connection disconnects this client and leaves the existing browser running.\n yield* Effect.addFinalizer(() => Effect.promise(() => browser.close()));\n const context = browser.contexts()[0];\n if (!context) return yield* Effect.fail(authFailed());\n const status = yield* saveContext(context);\n return { logged_in: true, source: 'galleton', session: status.id };\n});\n\nexport const login = Effect.fn('Auth.login')(function* () {\n const executable = yield* Config.option(Config.String('SEMRUSH_BROWSER_EXECUTABLE'));\n const browser = yield* Effect.acquireRelease(authCall(() => chromium.launch({ headless: false, executablePath: Option.getOrUndefined(executable) })), browser => Effect.promise(() => browser.close()));\n const context = yield* authCall(() => browser.newContext({ userAgent: 'OpenAI File Downloader, XaiImageApiFetch/1.0' }));\n const page = yield* authCall(() => context.newPage());\n yield* authCall(() => page.goto(origin + '/login/', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n yield* Effect.sync(() => process.stderr.write('Sign into Semrush in the browser. Waiting up to 5 minutes.\\n'));\n yield* authCall(() => page.waitForURL(url => url.hostname === 'www.semrush.com' && !url.pathname.startsWith('/login') && !url.pathname.startsWith('/signup'), { timeout: 300000 }));\n const status = yield* saveContext(context);\n return { logged_in: true, source: 'galleton', session: status.id };\n});\n"],"mappings":";;;;;;;;AAOA,MAAM,mBAAmB,IAAI,aAAa,EAAE,SAAS,6HAA6H,CAAC;AACnL,MAAM,YAAe,QAA0B,OAAO,WAAW;CAAE,KAAK;CAAK,OAAO;CAAY,CAAC;AAEjG,MAAM,cAAc,OAAO,GAAG,mBAAmB,CAAC,WAAW,SAAyB;CACpF,MAAM,WAAW,OAAO;CACxB,MAAM,OAAO,OAAO,OAAO,eAAe,eAAe,QAAQ,SAAS,CAAC,GAAE,WAAQ,OAAO,cAAcA,OAAK,OAAO,CAAC,CAAC;AACxH,QAAO,eAAe,KAAK,KAAK,SAAS,eAAe;EAAE,WAAW;EAAoB,SAAS;EAAO,CAAC,CAAC;CAC3G,MAAM,OAAO,OAAO,eAAe,KAAK,SAAS,YAAY;EAC3D,MAAM,WAAW,MAAM,MAAM,8BAA8B;GAAE,aAAa;GAAW,QAAQ,YAAY,QAAQ,KAAM;GAAE,CAAC;AAC1H,SAAO,SAAS,KAAK,SAAS,MAAM,GAAG;GACvC,CAAC;AACH,QAAO,OAAO,IAAI;EAAE,WAAW,cAAc,KAAK;EAAE,OAAO;EAAY,CAAC;CACxE,MAAM,UAAU,OAAO,eAAe,QAAQ,QAAQ,OAAO,CAAC;AAC9D,KAAI,CAAC,QAAQ,OAAQ,QAAO,OAAO,OAAO,KAAK,YAAY,CAAC;AAE5D,QAAO,OAAO,SAAS,KAAK;EAAE,UAAU;EAAW,eAAe;EAAQ,eAAe,QAAQ,KAAI,WAAU,GAAG,OAAO,KAAK,GAAG,OAAO,QAAQ,CAAC,KAAK,KAAK;EAAE,CAAC;EAC9J;AAEF,MAAa,gBAAgB,OAAO,GAAG,cAAc,CAAC,WAAW,KAAa;CAC5E,MAAM,QAAQ,OAAO,OAAO,IAAI;EAAE,WAAW;GAC3C,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,OAAI,OAAO,aAAa,WAAW,CAAC,CAAC,aAAa,QAAQ,CAAC,SAAS,OAAO,SAAS,IAAI,OAAO,YAAY,OAAO,SAAU,OAAM,IAAI,MAAM,eAAe;AAC3J,UAAO,OAAO;;EACb,aAAa,IAAI,WAAW,EAAE,SAAS,oFAAoF,CAAC;EAAE,CAAC;CAClI,MAAM,UAAU,OAAO,eAAe,SAAS,eAAe,OAAO,EAAE,SAAS,KAAO,CAAC,CAAC;AAEzF,QAAO,OAAO,mBAAmB,OAAO,cAAc,QAAQ,OAAO,CAAC,CAAC;CACvE,MAAM,UAAU,QAAQ,UAAU,CAAC;AACnC,KAAI,CAAC,QAAS,QAAO,OAAO,OAAO,KAAK,YAAY,CAAC;AAErD,QAAO;EAAE,WAAW;EAAM,QAAQ;EAAY,UAD/B,OAAO,YAAY,QAAQ,EACoB;EAAI;EAClE;AAEF,MAAa,QAAQ,OAAO,GAAG,aAAa,CAAC,aAAa;CACxD,MAAM,aAAa,OAAO,OAAO,OAAO,OAAO,OAAO,6BAA6B,CAAC;CACpF,MAAM,UAAU,OAAO,OAAO,eAAe,eAAe,SAAS,OAAO;EAAE,UAAU;EAAO,gBAAgB,OAAO,eAAe,WAAW;EAAE,CAAC,CAAC,GAAE,cAAW,OAAO,cAAcC,UAAQ,OAAO,CAAC,CAAC;CACvM,MAAM,UAAU,OAAO,eAAe,QAAQ,WAAW,EAAE,WAAW,gDAAgD,CAAC,CAAC;CACxH,MAAM,OAAO,OAAO,eAAe,QAAQ,SAAS,CAAC;AACrD,QAAO,eAAe,KAAK,KAAK,SAAS,WAAW;EAAE,WAAW;EAAoB,SAAS;EAAO,CAAC,CAAC;AACvG,QAAO,OAAO,WAAW,QAAQ,OAAO,MAAM,+DAA+D,CAAC;AAC9G,QAAO,eAAe,KAAK,YAAW,QAAO,IAAI,aAAa,qBAAqB,CAAC,IAAI,SAAS,WAAW,SAAS,IAAI,CAAC,IAAI,SAAS,WAAW,UAAU,EAAE,EAAE,SAAS,KAAQ,CAAC,CAAC;AAEnL,QAAO;EAAE,WAAW;EAAM,QAAQ;EAAY,UAD/B,OAAO,YAAY,QAAQ,EACoB;EAAI;EAClE"}
@@ -1,5 +1,5 @@
1
- import { a as reportParameters, l as webRequest, n as SessionError, s as rpcBody, t as ReportError } from "./errors-CU9T4acd.js";
2
- import { i as Sessions, n as origin } from "./session-layer-BDgTji_P.js";
1
+ import { a as reportParameters, l as webRequest, n as SessionError, s as rpcBody, t as ReportError } from "./errors-DNn_XeoU.js";
2
+ import { a as Sessions, r as origin } from "./session-layer-Bq5OpnR5.js";
3
3
  import { t as Semrush } from "./browser-service-BYYE-vSm.js";
4
4
  import { Config, Effect, Layer, Option, Predicate, Schema, Semaphore } from "effect";
5
5
  import { chromium } from "patchright";
@@ -19,7 +19,7 @@ const json = JSONbig({
19
19
  protoAction: "error",
20
20
  constructorAction: "error"
21
21
  });
22
- const safeBrowserError = () => new ReportError({ message: "Semrush browser request failed. Run semrush-cli doctor. If your session expired, sign in again with semrush-cli auth login." });
22
+ const safeBrowserError = () => new ReportError({ message: "Semrush browser request failed. Run semrush doctor. If your session expired, sign in again with semrush auth login." });
23
23
  const browserCall = (run) => Effect.tryPromise({
24
24
  try: run,
25
25
  catch: safeBrowserError
@@ -74,7 +74,7 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
74
74
  }));
75
75
  const auth = yield* Effect.try({
76
76
  try: () => readBootstrap(bootstrap.text),
77
- catch: () => new SessionError({ message: "Semrush sign-in metadata unavailable. Run semrush-cli auth login or import a signed-in browser." })
77
+ catch: () => new SessionError({ message: "Semrush sign-in metadata unavailable. Run semrush auth login or import a signed-in browser." })
78
78
  });
79
79
  const semaphore = yield* Semaphore.make(1);
80
80
  let spectrumToken = "";
@@ -83,21 +83,21 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
83
83
  const payload$1 = JSON.parse(encodedPayload);
84
84
  const query = new URLSearchParams();
85
85
  const get = method$1 === "GET";
86
- if (get) for (const [key, value] of Object.entries(payload$1)) for (const item of Array.isArray(value) ? value : [value]) query.append(key, String(item));
86
+ if (get) for (const [key, value] of Object.entries(payload$1 ?? {})) for (const item of Array.isArray(value) ? value : [value]) query.append(key, String(item));
87
87
  const options = {
88
88
  method: method$1,
89
89
  credentials: "include",
90
90
  headers: extraHeaders$1,
91
91
  signal: AbortSignal.timeout(45e3)
92
92
  };
93
- if (!get && method$1 !== "DELETE") {
93
+ if (!get && method$1 !== "DELETE" && payload$1 !== null) {
94
94
  options.headers = {
95
95
  ...extraHeaders$1,
96
96
  "Content-Type": "application/json"
97
97
  };
98
98
  options.body = JSON.stringify(payload$1);
99
99
  }
100
- const response = await fetch(get && query.size > 0 ? path$1 + "?" + query : path$1, options);
100
+ const response = await fetch(get && query.size > 0 ? path$1 + (path$1.includes("?") ? "&" : "?") + query : path$1, options);
101
101
  return {
102
102
  status: response.status,
103
103
  text: await response.text()
@@ -124,7 +124,7 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
124
124
  try: () => parseWire(wire.text),
125
125
  catch: () => new ReportError({ message: "Semrush returned a page or challenge instead of JSON. Sign in again or retry later." })
126
126
  });
127
- if (objectValue(body) && ("error" in body || "status" in body && !(body.status === null || body.status === "SUCCESS" || body.status === "success"))) return yield* Effect.fail(new ReportError({ message: `Semrush report failed: ${JSON.stringify(redact(body.error ?? body.status ?? null, [
127
+ if (objectValue(body) && ("error" in body || !path.startsWith("/siteaudit/") && "status" in body && !(body.status === null || body.status === "SUCCESS" || body.status === "success"))) return yield* Effect.fail(new ReportError({ message: `Semrush report failed: ${JSON.stringify(redact(body.error ?? body.status ?? null, [
128
128
  auth.apiKey,
129
129
  spectrumToken,
130
130
  spectrumToken.replace(/^Bearer /, "")
@@ -159,6 +159,18 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
159
159
  if (!objectValue(body) || !("result" in body)) return yield* Effect.fail(new ReportError({ message: `Semrush ${method}: missing report result.` }));
160
160
  return body.result ?? null;
161
161
  });
162
+ const checkAuditSettings = Effect.fn("Semrush.checkAuditSettings")(function* (name, query) {
163
+ if (name === "audit_create") {
164
+ const campaigns = yield* request("/siteaudit/api/campaigns/seolist", { key: auth.apiKey }, "GET");
165
+ if (!Array.isArray(campaigns)) return yield* Effect.fail(new ReportError({ message: "Site Audit campaign discovery returned an unexpected response. No settings were saved." }));
166
+ if (campaigns.some((campaign) => objectValue(campaign) && String(campaign.id) === String(query.campaign_id))) return yield* Effect.fail(new ReportError({ message: "This SEO project already has a Site Audit. Use audit-configure to replace its settings explicitly." }));
167
+ }
168
+ if (name === "audit_configure") {
169
+ const current = yield* request(`/siteaudit/api/campaigns/${query.campaign_id}/info?key=${encodeURIComponent(auth.apiKey)}`, {}, "GET");
170
+ if (objectValue(current) && Predicate.isString(current.projectDomain) && current.projectDomain !== query.domain) return yield* Effect.fail(new ReportError({ message: "The audit domain must match the SEO project website. Inspect audit-info for projectDomain before configuring it." }));
171
+ if (objectValue(current) && (Boolean(current.authEnabled) || Boolean(current.webBotAuthEnabled) || objectValue(current.gaSettings) && Object.keys(current.gaSettings).length > 0)) return yield* Effect.fail(new ReportError({ message: "This audit uses protected-site authentication or Google Analytics. Edit its settings in the Semrush web UI to preserve those connections." }));
172
+ }
173
+ });
162
174
  const report = Effect.fn("Semrush.report")(function* ({ name, definition, query }) {
163
175
  if (!(yield* browserCall(() => page.evaluate(async () => {
164
176
  const response = await fetch("/home/", {
@@ -168,9 +180,9 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
168
180
  signal: AbortSignal.timeout(15e3)
169
181
  });
170
182
  return response.ok && new URL(response.url).pathname === "/home/";
171
- })))) return yield* Effect.fail(new SessionError({ message: "Semrush session expired. Run semrush-cli auth login or import a signed-in browser." }));
183
+ })))) return yield* Effect.fail(new SessionError({ message: "Semrush session expired. Run semrush auth login or import a signed-in browser." }));
172
184
  const family = definition.transport;
173
- const planned = family === "project_web" ? webRequest(definition, query) : void 0;
185
+ const planned = ["project_web", "audit_web"].includes(family) ? webRequest(definition, query) : void 0;
174
186
  const params = planned?.body ?? reportParameters(definition, query);
175
187
  let reportData;
176
188
  if (planned) {
@@ -179,7 +191,8 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
179
191
  if (!objectValue(current) || !objectValue(current.folder)) return yield* Effect.fail(new ReportError({ message: "Project metadata unavailable. Inspect the project before adding a website." }));
180
192
  if (Predicate.isString(current.folder.domain) && current.folder.domain.length > 0) return yield* Effect.fail(new ReportError({ message: "This project already has a website. The Semrush web UI only allows adding a website when none is set." }));
181
193
  }
182
- reportData = yield* request(planned.path, planned.body, planned.method);
194
+ if (family === "audit_web") yield* checkAuditSettings(name, query);
195
+ reportData = yield* request(family === "audit_web" ? planned.path + "?key=" + encodeURIComponent(auth.apiKey) : planned.path, planned.body, planned.method);
183
196
  } else if ([
184
197
  "keyword",
185
198
  "domain",
@@ -297,4 +310,4 @@ const captureCookies = Effect.fn("Semrush.captureCookies")(function* (context, c
297
310
 
298
311
  //#endregion
299
312
  export { semrushLive as n, readBootstrap as t };
300
- //# sourceMappingURL=browser-layer-BJ4eQAUz.js.map
313
+ //# sourceMappingURL=browser-layer-gdm03TCV.js.map