semrush-cli 0.4.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,233 +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
- 89 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
29
+
30
+ requires node.js 22 or newer. linux, macos and windows are supported.
32
31
 
33
- ```sh
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'
37
+ ```
38
+
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`.
42
+
43
+ ### auth
44
+
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
51
+ ```
52
+
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
38
58
  ```
39
59
 
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`.
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.
44
61
 
45
- To import an existing, authorized Chromium session on a headless machine:
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.
46
63
 
47
- ```sh
48
- semrush-cli auth import --cdp-url http://127.0.0.1:9234
49
- semrush-cli doctor --json
64
+ ```bash
65
+ semrush auth logout
66
+ semrush daemon stop
67
+ semrush daemon start
50
68
  ```
51
69
 
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.
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.
56
71
 
57
- ## projects
72
+ ## command surface
58
73
 
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
74
+ 89 operations, available as cli commands and mcp tools.
75
+
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
68
100
  ```
69
101
 
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).
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.
103
+
104
+ ## examples
76
105
 
77
- ## keyword lists
106
+ ### keyword research
78
107
 
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
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'
86
112
  ```
87
113
 
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).
114
+ pull out the numbers you care about:
92
115
 
93
- ## site audit
116
+ ```bash
117
+ semrush keyword-overview --keyword 'email marketing' \
118
+ | jq '.data | {phrase, volume, difficulty, cpc}'
119
+ ```
120
+
121
+ ### competitors and backlinks
94
122
 
95
- ```sh
96
- semrush-cli audit-create --campaign-id 456 --name "Example audit" --domain example.com --page-limit 100 --confirm
97
- semrush-cli audit-start --campaign-id 456 --confirm
98
- semrush-cli audit-snapshots --campaign-id 456
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"]'
99
129
  ```
100
130
 
101
- [Site Audit guide](https://semrush.micr.dev/docs/site-audit). Configuration and launch are separate. Defaults use manual scheduling and no email.
131
+ ### projects
132
+
133
+ preview a write before sending it:
102
134
 
135
+ ```bash
136
+ semrush project-create --name 'my website' --domain example.com --dry-run
137
+ ```
103
138
 
104
- ## commands
139
+ then create it when you're ready:
105
140
 
106
- ```sh
107
- semrush-cli keyword-overview --keyword 'email marketing' --json
108
- semrush-cli keyword-magic --keyword 'email marketing' --questions-only --page-size 20 --json
109
- semrush-cli domain-keywords --domain openai.com --page 2 --page-size 20 --json
110
- semrush-cli domain-competitors --domain openai.com --page-size 10 --json
111
- semrush-cli keyword-gap --domains '["openai.com","anthropic.com"]' --json
112
- semrush-cli backlink-gap --domains '["openai.com","anthropic.com"]' --json
113
- semrush-cli backlink-links --domain openai.com --filters '+|type||nofollow' --json
114
- semrush-cli domain-ai-visibility --domain openai.com --json
115
- semrush-cli snapshot-dates --database us --json
116
- semrush-cli reports --json
117
- semrush-cli domain-keywords --help
141
+ ```bash
142
+ semrush project-create --name 'my website' --domain example.com --confirm
118
143
  ```
119
144
 
120
- | Area | Reports |
121
- | --- | --- |
122
- | Keywords | Overview, ideas, questions, SERPs, ads, clusters, paginated Keyword Magic and groups |
123
- | Organic | Rankings, pages, subdomains, changes, competitors, topics and trends |
124
- | Gaps | Keyword overlaps and gaps, backlink prospects and profile comparisons |
125
- | Backlinks | Links, referring domains, anchors, indexed pages, outbound domains, history and authority |
126
- | AI visibility | Visibility, mentions, citation sources and country distribution |
127
- | Keyword lists | Create, read, rename/delete, import, metrics, tags, deletion/undo, clustering and structured strategy generation |
128
- | Projects | Create, inspect, rename, delete, leave, tags, pinning, toolkit status, dashboard metrics and explicit SEO setup |
129
- | Site Audit | Create/configure, explicit launch, status, limits, history and snapshots |
130
- | Account | Limits, permissions, databases, snapshots, folders, saved lists and audit campaigns |
131
-
132
- All successful reports use `{authenticated: true, report, data}`. Domain ranking
133
- and page reports include provider totals. Large integers remain exact as decimal
134
- strings. Errors go to stderr with a nonzero exit. `--json` works before or after
135
- the command. Objects and arrays use JSON flags; booleans support explicit `false`.
136
- See the [full command reference](https://semrush.micr.dev/docs/reports).
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.
137
183
 
138
184
  ## mcp
139
185
 
140
- 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:
141
187
 
142
188
  ```json
143
189
  {
144
190
  "mcpServers": {
145
191
  "semrush": {
146
- "command": "semrush-cli",
192
+ "command": "semrush",
147
193
  "args": ["mcp"]
148
194
  }
149
195
  }
150
196
  }
151
197
  ```
152
198
 
153
- Use an absolute executable path if your client does not inherit the terminal's
154
- PATH. `npx -y semrush-cli@0.4.0 mcp` also works. MCP tool names use underscores,
155
- for example `keyword_overview` and `domain_keywords`. Input schemas and reports
156
- match the CLI. The 67 read tools carry read-only annotations. The 22 write tools require
157
- `confirm:true`, or accept `dry_run:true` to preview the request. Delete and leave
158
- also carry destructive annotations. Login, arbitrary URLs and JavaScript
159
- execution are not MCP tools.
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.
160
200
 
161
- ## sessions
201
+ ## what it looks like
162
202
 
163
- ```sh
164
- semrush-cli auth status --check --json
165
- semrush-cli auth logout
166
- semrush-cli daemon stop
167
- semrush-cli daemon start
168
- ```
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.
169
204
 
170
- Galleton stores credentials under `~/.local/share/semrush-cli/galleton`. The
171
- private daemon listens on `127.0.0.1:8771`. It continues after the terminal closes;
172
- run `daemon start` after reboot. The CLI installs no system startup service.
205
+ ## building from source
173
206
 
174
- Each CLI process restores its encrypted session into a fresh, transient Chromium
175
- context. The MCP server reuses one context and serializes requests. Successful
176
- reports capture cookie updates back to Galleton. Session expiry requires sign-in
177
- or import again. No anonymous fallback or automatic request retries.
178
-
179
- The unchanged Galleton TypeScript SDK is vendored at a pinned upstream commit
180
- because it is not on npm. The bundled daemon adds a tested `static` provider for
181
- storage without renewal. Semrush has no proven renewal contract in the reference
182
- integration, so this tool does not invent one. See [architecture](docs/architecture.md)
183
- and [SDK provenance](src/vendor/README.md).
184
-
185
- | Variable | Default |
186
- | --- | --- |
187
- | `SEMRUSH_GALLETON_URL` | `http://127.0.0.1:8771` |
188
- | `SEMRUSH_GALLETON_TOKEN_FILE` | `~/.local/share/semrush-cli/galleton/api.token` |
189
- | `SEMRUSH_SESSION` | `semrush-cli` |
190
- | `SEMRUSH_BROWSER_EXECUTABLE` | Patchright's Chromium |
191
-
192
- The setup/daemon commands manage only the default CLI-owned daemon. An external
193
- daemon must support the documented static-provider extension. Never expose its
194
- 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
+ ```
195
215
 
196
- ## limits
216
+ the source build uses go to build the pinned galleton engine. npm releases include the engine for supported platforms.
197
217
 
198
- Semrush account permissions, quotas and product access apply. A read-only report
199
- can consume an allowance. Internal web endpoints may change. Provider caps remain
200
- visible; there is no unlimited-export claim. Galleton encrypts at rest but its key
201
- and vault share a private local directory. Other processes running as your OS user
202
- are within the same trust boundary.
218
+ ## documentation
203
219
 
204
- ## 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.
205
221
 
206
- ```sh
207
- npm ci
208
- npm run check
209
- npm --prefix docs-site ci
210
- npm --prefix docs-site run build
211
- ```
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)
212
226
 
213
- Development builds require Go 1.23+ and `tar`/`patch` to build the pinned Galleton
214
- daemon with its storage-only extension. The build runs the upstream core suite
215
- with the extension's integration test. Published users do not need Go.
227
+ ## contributing
216
228
 
217
- Tests compare all 48 request shapes against the Python reference, use real
218
- Chromium and Galleton with a local HTTP fixture, and exercise the built executable.
219
- They never require a Semrush account. See [verification](docs/verification.md) for
220
- 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.
221
230
 
222
- ## uninstall
231
+ ## disclaimer
223
232
 
224
- ```sh
225
- semrush-cli auth logout
226
- semrush-cli daemon stop
227
- npm uninstall -g semrush-cli
228
- ```
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.
229
234
 
230
- The private vault and browser cache remain on disk. Remove them only after
231
- checking they contain nothing you need.
235
+ ## license
232
236
 
233
- 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-BjuHkMt_.js";
2
- import { a as Sessions, r as origin } from "./session-layer-DJzN9Xx2.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-DgW0vW92.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-CHCXZZpR.js.map
82
+ //# sourceMappingURL=auth-CrYOA8QT.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth-CHCXZZpR.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":"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"}
@@ -1,5 +1,5 @@
1
- import { a as reportParameters, l as webRequest, n as SessionError, s as rpcBody, t as ReportError } from "./errors-BjuHkMt_.js";
2
- import { a as Sessions, r as origin } from "./session-layer-DJzN9Xx2.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 = "";
@@ -180,7 +180,7 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
180
180
  signal: AbortSignal.timeout(15e3)
181
181
  });
182
182
  return response.ok && new URL(response.url).pathname === "/home/";
183
- })))) 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." }));
184
184
  const family = definition.transport;
185
185
  const planned = ["project_web", "audit_web"].includes(family) ? webRequest(definition, query) : void 0;
186
186
  const params = planned?.body ?? reportParameters(definition, query);
@@ -310,4 +310,4 @@ const captureCookies = Effect.fn("Semrush.captureCookies")(function* (context, c
310
310
 
311
311
  //#endregion
312
312
  export { semrushLive as n, readBootstrap as t };
313
- //# sourceMappingURL=browser-layer-DgW0vW92.js.map
313
+ //# sourceMappingURL=browser-layer-gdm03TCV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-layer-gdm03TCV.js","names":["browser","payload: Schema.JsonObject | null","method","payload","options: RequestInit","extraHeaders","path","extra: Record<string, string>","reportData: Schema.Json | undefined","request"],"sources":["../src/browser-layer.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { Config, Effect, Layer, Option, Semaphore, Schema, Predicate } from 'effect';\nimport { chromium, type BrowserContext, type Page } from 'patchright';\nimport JSONbig from 'json-bigint';\nimport { Semrush } from './browser-service.js';\nimport { Sessions } from './session-service.js';\nimport { origin } from './session-layer.js';\nimport { ReportError, SessionError } from './errors.js';\nimport { reportParameters, webRequest, rpcBody, type Query } from './report-parameters.js';\nimport type { ReportRequest } from './validation.js';\n\nconst transportPaths = new Map([['domain', '/dpa/rpc'], ['direct', '/dpa/rpc'], ['magic', '/kmtgw/v2/webapi'], ['keyword', '/kwogw/v2/webapi'], ['kwm', '/kwm/rpc']]);\nconst json = JSONbig({ storeAsString: true, protoAction: 'error', constructorAction: 'error' });\nconst safeBrowserError = () => new ReportError({ message: 'Semrush browser request failed. Run semrush doctor. If your session expired, sign in again with semrush auth login.' });\nconst browserCall = <A>(run: () => Promise<A>) => Effect.tryPromise({ try: run, catch: safeBrowserError });\nconst objectValue = (value: Schema.Json | undefined): value is Schema.JsonObject => Predicate.isObject(value) && !Array.isArray(value);\n\n/** JSON never passes through the browser's number parser, preserving 64-bit IDs. */\nexport function parseWire(text: string): Schema.Json { return Schema.decodeUnknownSync(Schema.Json)(json.parse(text)); }\n\n/** Remove credential fields and echoed secrets, including nested provider errors. */\nexport function redact(value: Schema.Json, secrets: string[]): Schema.Json {\n if (Array.isArray(value)) return value.map(item => redact(item, secrets));\n if (objectValue(value)) return Object.fromEntries(Object.entries(value).filter(([key]) => !/^(api_?key|key|user_?id|access_token|refresh_token|authorization|cookie|set-cookie)$/i.test(key)).map(([key, item]) => [key, redact(item, secrets)]));\n if (Predicate.isString(value)) return secrets.filter(secret => secret.length > 0).reduce((text, secret) => text.replaceAll(secret, '[redacted]'), value);\n return value;\n}\n\nexport const makeSemrushLayer = (browserOrigin: string) => Layer.effect(Semrush, Effect.gen(function* () {\n const sessions = yield* Sessions;\n const executable = yield* Config.option(Config.String('SEMRUSH_BROWSER_EXECUTABLE'));\n const headers = yield* sessions.headers();\n const browser = yield* Effect.acquireRelease(browserCall(() => chromium.launch({ headless: true, executablePath: Option.getOrUndefined(executable) })), browser => Effect.promise(() => browser.close()));\n const context = yield* browserCall(() => browser.newContext({ userAgent: 'OpenAI File Downloader, XaiImageApiFetch/1.0' }));\n const cookieHeader = headers.Cookie ?? headers.cookie ?? '';\n yield* browserCall(() => context.addCookies(cookieHeader.split(';').filter(cookie => cookie.includes('=')).map(cookie => {\n const separator = cookie.indexOf('=');\n return { name: cookie.slice(0, separator).trim(), value: cookie.slice(separator + 1).trim(), url: browserOrigin, httpOnly: true, secure: browserOrigin.startsWith('https:'), sameSite: 'Lax' as const };\n })));\n const page = yield* browserCall(() => context.newPage());\n yield* browserCall(() => page.goto(browserOrigin + '/robots.txt', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n const bootstrap = yield* browserCall(() => page.evaluate(async () => {\n const response = await fetch('/analytics/overview/?db=us', { credentials: 'include', signal: AbortSignal.timeout(45000) });\n return { status: response.status, text: await response.text() };\n }));\n // Extract the JSON value with a balanced scanner, never eval page-owned JavaScript.\n const auth = yield* Effect.try({ try: () => readBootstrap(bootstrap.text), catch: () => new SessionError({ message: 'Semrush sign-in metadata unavailable. Run semrush auth login or import a signed-in browser.' }) });\n const semaphore = yield* Semaphore.make(1);\n let spectrumToken = '';\n const request = Effect.fn('Semrush.request')(function* (path: string, payload: Query | null, method = 'POST', extraHeaders: Record<string, string> = {}) {\n const wire = yield* browserCall(() => page.evaluate(async ({ path, encodedPayload, method, extraHeaders }) => {\n const payload: Schema.JsonObject | null = JSON.parse(encodedPayload);\n const query = new URLSearchParams();\n const get = method === 'GET';\n if (get) for (const [key, value] of Object.entries(payload ?? {})) for (const item of Array.isArray(value) ? value : [value]) query.append(key, String(item));\n const options: RequestInit = { method, credentials: 'include', headers: extraHeaders, signal: AbortSignal.timeout(45000) };\n if (!get && method !== 'DELETE' && payload !== null) { options.headers = { ...extraHeaders, 'Content-Type': 'application/json' }; options.body = JSON.stringify(payload); }\n const response = await fetch(get && query.size > 0 ? path + (path.includes('?') ? '&' : '?') + query : path, options);\n return { status: response.status, text: await response.text() };\n }, { path, encodedPayload: JSON.stringify(payload), method, extraHeaders }));\n if (wire.status < 200 || wire.status >= 300) {\n const detail = yield* Effect.try({ try: () => JSON.stringify(redact(parseWire(wire.text), [auth.apiKey, spectrumToken, spectrumToken.replace(/^Bearer /, '')])).slice(0, 500), catch: safeBrowserError }).pipe(Effect.option);\n return yield* Effect.fail(new ReportError({ message: `Semrush request returned HTTP ${wire.status}.${Option.isSome(detail) ? ' ' + detail.value : ' Check your session and account permissions.'}` }));\n }\n if (wire.status === 204 && !wire.text) return null;\n const body = yield* Effect.try({ try: () => parseWire(wire.text), catch: () => new ReportError({ message: 'Semrush returned a page or challenge instead of JSON. Sign in again or retry later.' }) });\n 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, [auth.apiKey, spectrumToken, spectrumToken.replace(/^Bearer /, '')])).slice(0, 500)}` }));\n return body;\n });\n const rpc = Effect.fn('Semrush.rpc')(function* (method: string, params: Query, transport = 'keyword', webReport = 'domain.overview') {\n const payload = transport === 'domain' ? { request_id: randomUUID(), report: webReport, args: params, ...auth } : transport === 'direct' ? { ...params, ...auth } : params;\n const rpcParams = transport === 'kwm' ? { ...payload, ...(method.startsWith('user.') ? { api_key: auth.apiKey, user_id: auth.userId } : { APIKey: auth.apiKey, UserID: auth.userId }) } : payload;\n const extra: Record<string, string> = transport === 'kwm' ? { 'api-key': auth.apiKey, 'user-id': String(auth.userId) } : {};\n const body = yield* request(transportPaths.get(transport) ?? '/kwogw/v2/webapi', rpcBody(method, rpcParams), 'POST', extra);\n if (!objectValue(body) || !('result' in body)) return yield* Effect.fail(new ReportError({ message: `Semrush ${method}: missing report result.` }));\n return body.result ?? null;\n });\n const checkAuditSettings = Effect.fn('Semrush.checkAuditSettings')(function* (name: string, query: Query) {\n if (name === 'audit_create') {\n const campaigns = yield* request('/siteaudit/api/campaigns/seolist', { key: auth.apiKey }, 'GET');\n if (!Array.isArray(campaigns)) return yield* Effect.fail(new ReportError({ message: 'Site Audit campaign discovery returned an unexpected response. No settings were saved.' }));\n 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.' }));\n }\n // The audit save endpoint replaces settings, including protected-site credentials.\n if (name === 'audit_configure') {\n const current = yield* request(`/siteaudit/api/campaigns/${query.campaign_id}/info?key=${encodeURIComponent(auth.apiKey)}`, {}, 'GET');\n 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.' }));\n 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.' }));\n }\n });\n const report = Effect.fn('Semrush.report')(function* ({ name, definition, query }: ReportRequest) {\n const valid = yield* browserCall(() => page.evaluate(async () => {\n const response = await fetch('/home/', { method: 'HEAD', credentials: 'include', cache: 'no-store', signal: AbortSignal.timeout(15000) });\n return response.ok && new URL(response.url).pathname === '/home/';\n }));\n if (!valid) return yield* Effect.fail(new SessionError({ message: 'Semrush session expired. Run semrush auth login or import a signed-in browser.' }));\n const family = definition.transport;\n const planned = ['project_web', 'audit_web'].includes(family) ? webRequest(definition, query) : undefined;\n const params = planned?.body ?? reportParameters(definition, query);\n let reportData: Schema.Json | undefined;\n if (planned) {\n // The web UI only allows adding a website to a project that has none.\n if (name === 'project_update' && query.domain !== undefined) {\n const current = yield* request('/apis/v4-raw/folders/api/v0/folders/folder', { folder_id: query.folder_id ?? null }, 'GET');\n if (!objectValue(current) || !objectValue(current.folder)) return yield* Effect.fail(new ReportError({ message: 'Project metadata unavailable. Inspect the project before adding a website.' }));\n 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.' }));\n }\n if (family === 'audit_web') yield* checkAuditSettings(name, query);\n const path = family === 'audit_web' ? planned.path + '?key=' + encodeURIComponent(auth.apiKey) : planned.path;\n reportData = yield* request(path, planned.body, planned.method);\n } else if (['keyword', 'domain', 'direct', 'magic', 'kwm'].includes(family)) reportData = yield* rpc(definition.method, params, family, definition.web_report);\n else {\n const extra: Record<string, string> = {};\n if (family === 'gap') {\n if (!spectrumToken) spectrumToken = yield* browserCall(() => observeSpectrum(page, browserOrigin));\n Object.assign(extra, { authorization: spectrumToken, 'x-report': 'gap.keywords', 'x-request-id': randomUUID() });\n }\n let path = definition.method;\n if (family.startsWith('backlink')) {\n path = '/analytics/backlinks/webapi2/';\n params.key = auth.apiKey;\n if (family === 'backlink_overview') {\n path += 'overview/' + definition.method;\n extra['x-request-id'] = randomUUID();\n yield* request('/analytics/backlinks/webapi2/overview/init', params, 'GET', extra);\n }\n } else if (path.startsWith('/siteaudit/')) params.key = auth.apiKey;\n reportData = yield* request(path, params, ['post', 'gap', 'projects'].includes(family) ? 'POST' : 'GET', extra);\n }\n if (definition.total_method) {\n const totalParams = name === 'domain_position_changes' ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== 'display')) : params;\n const total = yield* rpc(definition.total_method, totalParams, 'domain', definition.web_report);\n reportData = { rows: reportData, total, page: query.page ?? null, page_size: query.page_size ?? null };\n }\n if (name === 'keyword_overview') {\n const rows = objectValue(reportData) ? reportData.keywords : undefined;\n reportData = Array.isArray(rows) ? rows.find(item => objectValue(item) && item.database === query.database) : undefined;\n if (!reportData) return yield* Effect.fail(new ReportError({ message: 'No keyword data for the selected database.' }));\n } else if (name === 'domain_overview') {\n const search = Array.isArray(reportData) ? reportData.find(item => objectValue(item) && item.database === query.database) : undefined;\n if (!search) return yield* Effect.fail(new ReportError({ message: 'No domain data for the selected database.' }));\n reportData = { search, backlinks: yield* rpc('backlinks.Summary', { searchItem: query.domain ?? null, searchType: 'domain' }, 'domain') };\n }\n if (Predicate.isNumber(query.limit) && Array.isArray(reportData)) reportData = reportData.slice(0, query.limit);\n yield* captureCookies(context, sessions.capture, browserOrigin);\n return { authenticated: true as const, report: name, data: redact(reportData ?? null, [auth.apiKey, spectrumToken, spectrumToken.replace(/^Bearer /, '')]) };\n });\n return Semrush.of({ report: request => semaphore.withPermits(1)(report(request)) });\n}));\n\nexport const semrushLive = makeSemrushLayer(origin);\n\ninterface BootstrapAuth { readonly apiKey: string; readonly userId: string | number; }\nexport function readBootstrap(html: string): BootstrapAuth {\n const match = /window\\.sm2\\.user\\s*=\\s*/.exec(html);\n if (!match) throw new Error('No bootstrap');\n const start = match.index + match[0].length;\n let depth = 0, quoted = false, escaped = false;\n for (let index = start; index < html.length; index++) {\n const char = html[index];\n if (quoted) {\n if (escaped) escaped = false;\n else if (char === '\\\\') escaped = true;\n else if (char === '\"') quoted = false;\n } else if (char === '\"') quoted = true;\n else if (char === '{') depth++;\n else if (char === '}' && --depth === 0) {\n const user = parseWire(html.slice(start, index + 1));\n if (objectValue(user) && Predicate.isString(user.api_key) && (Predicate.isString(user.id) || Predicate.isNumber(user.id))) return { apiKey: user.api_key, userId: user.id };\n break;\n }\n }\n throw new Error('Invalid bootstrap');\n}\n\nasync function observeSpectrum(page: Page, browserOrigin: string): Promise<string> {\n try {\n // Observe and navigate together so either timeout is handled immediately.\n const [observed] = await Promise.all([\n page.waitForRequest(request => request.url().startsWith(browserOrigin + '/spectrum/') && Boolean(request.headers().authorization), { timeout: 20000 }),\n page.goto(browserOrigin + '/analytics/keywordgap/?db=us', { waitUntil: 'domcontentloaded', timeout: 45000 }),\n ]);\n return observed.headers().authorization ?? '';\n } finally {\n await page.goto(browserOrigin + '/robots.txt', { waitUntil: 'domcontentloaded', timeout: 45000 });\n }\n}\n\nconst captureCookies = Effect.fn('Semrush.captureCookies')(function* (context: BrowserContext, capture: (cookies: string[]) => Effect.Effect<void, SessionError>, cookieOrigin: string) {\n const cookies = yield* browserCall(() => context.cookies(cookieOrigin));\n yield* capture(cookies.map(cookie => `${cookie.name}=${cookie.value}; Path=${cookie.path}${cookieOrigin.startsWith('https:') ? '; Secure' : ''}${cookie.httpOnly ? '; HttpOnly' : ''}${cookie.expires > 0 ? '; Expires=' + new Date(cookie.expires * 1000).toUTCString() : ''}`));\n});\n"],"mappings":"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"}