semrush-cli 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Microck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,166 @@
1
- # Temporary Holding Version
1
+ <p align="center"><img src="assets/logo.svg" width="100" alt="semrush-cli"></p>
2
+ <h1 align="center">semrush-cli</h1>
3
+ <p align="center">Semrush research from your terminal and MCP clients.</p>
4
+ <p align="center"><a href="https://semrush.micr.dev">Documentation</a> · <a href="https://www.npmjs.com/package/semrush-cli">npm</a> · <a href="https://github.com/Microck/semrush-cli/issues">Issues</a></p>
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ The executable is `semrush-cli`. It can coexist with other tools named `semrush`.
7
+
8
+ 48 read-only web reports for keyword research, organic rankings, competitors,
9
+ keyword and backlink gaps, backlinks, historical/mobile research and AI visibility.
10
+ Written in TypeScript with Effect. Sessions live in an encrypted Galleton vault.
11
+ No Semrush developer API key, Python runtime or CAPTCHA service required.
12
+
13
+ ## Install
14
+
15
+ Requires Node.js 22+. Includes Galleton binaries for Linux x64/arm64,
16
+ macOS x64/arm64 and Windows x64.
17
+
18
+ ```sh
19
+ npm install -g semrush-cli
20
+ semrush-cli setup
21
+ semrush-cli auth login
22
+ semrush-cli domain-overview --domain openai.com --json
23
+ ```
24
+
25
+ `setup` installs Chromium and starts a private Galleton daemon. `auth login`
26
+ opens an isolated browser for sign-in, verifies the session, and stores it in
27
+ Galleton. It requires a graphical desktop. On Linux, missing Chromium system
28
+ libraries can be installed with `npx patchright install-deps chromium`.
29
+
30
+ To import an existing, authorized Chromium session on a headless machine:
31
+
32
+ ```sh
33
+ semrush-cli auth import --cdp-url http://127.0.0.1:9234
34
+ semrush-cli doctor --json
35
+ ```
36
+
37
+ The browser must already expose CDP on a literal loopback HTTP address. This
38
+ supports the browser managed by [semrush-web-mcp](https://github.com/Microck/semrush-web-mcp).
39
+ Import does not stop or change that browser. Keep CDP private; forward it over
40
+ SSH when necessary.
41
+
42
+ ## Research
43
+
44
+ ```sh
45
+ semrush-cli keyword-overview --keyword 'email marketing' --json
46
+ semrush-cli keyword-magic --keyword 'email marketing' --questions-only --page-size 20 --json
47
+ semrush-cli domain-keywords --domain openai.com --page 2 --page-size 20 --json
48
+ semrush-cli domain-competitors --domain openai.com --page-size 10 --json
49
+ semrush-cli keyword-gap --domains '["openai.com","anthropic.com"]' --json
50
+ semrush-cli backlink-gap --domains '["openai.com","anthropic.com"]' --json
51
+ semrush-cli backlink-links --domain openai.com --filters '+|type||nofollow' --json
52
+ semrush-cli domain-ai-visibility --domain openai.com --json
53
+ semrush-cli snapshot-dates --database us --json
54
+ semrush-cli reports --json
55
+ semrush-cli domain-keywords --help
56
+ ```
57
+
58
+ | Area | Reports |
59
+ | --- | --- |
60
+ | Keywords | Overview, ideas, questions, SERPs, ads, clusters, paginated Keyword Magic and groups |
61
+ | Organic | Rankings, pages, subdomains, changes, competitors, topics and trends |
62
+ | Gaps | Keyword overlaps and gaps, backlink prospects and profile comparisons |
63
+ | Backlinks | Links, referring domains, anchors, indexed pages, outbound domains, history and authority |
64
+ | AI visibility | Visibility, mentions, citation sources and country distribution |
65
+ | Account | Limits, permissions, databases, snapshots, projects, folders, saved lists and audit campaigns |
66
+
67
+ All successful reports use `{authenticated: true, report, data}`. Domain ranking
68
+ and page reports include provider totals. Large integers remain exact as decimal
69
+ strings. Errors go to stderr with a nonzero exit. `--json` works before or after
70
+ the command. Objects and arrays use JSON flags; booleans support explicit `false`.
71
+ See the [full command reference](https://semrush.micr.dev/docs/reports).
72
+
73
+ ## MCP
74
+
75
+ Complete setup and authentication, then configure your MCP client:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "semrush": {
81
+ "command": "semrush-cli",
82
+ "args": ["mcp"]
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ Use an absolute executable path if your client does not inherit the terminal's
89
+ PATH. `npx -y semrush-cli@0.1.1 mcp` also works. MCP tool names use underscores,
90
+ for example `keyword_overview` and `domain_keywords`. Input schemas and reports
91
+ match the CLI. All 48 tools are annotated read-only. Login, account mutations,
92
+ arbitrary URLs and JavaScript execution are not MCP tools.
93
+
94
+ ## Sessions
95
+
96
+ ```sh
97
+ semrush-cli auth status --check --json
98
+ semrush-cli auth logout
99
+ semrush-cli daemon stop
100
+ semrush-cli daemon start
101
+ ```
102
+
103
+ Galleton stores credentials under `~/.local/share/semrush-cli/galleton`. The
104
+ private daemon listens on `127.0.0.1:8768`. It continues after the terminal closes;
105
+ run `daemon start` after reboot. The CLI installs no system startup service.
106
+
107
+ Each CLI process restores its encrypted session into a fresh, transient Chromium
108
+ context. The MCP server reuses one context and serializes requests. Successful
109
+ reports capture cookie updates back to Galleton. Session expiry requires sign-in
110
+ or import again. No anonymous fallback or automatic request retries.
111
+
112
+ The unchanged Galleton TypeScript SDK is vendored at a pinned upstream commit
113
+ because it is not on npm. The bundled daemon adds a tested `static` provider for
114
+ storage without renewal. Semrush has no proven renewal contract in the reference
115
+ integration, so this tool does not invent one. See [architecture](docs/architecture.md)
116
+ and [SDK provenance](src/vendor/README.md).
117
+
118
+ | Variable | Default |
119
+ | --- | --- |
120
+ | `SEMRUSH_GALLETON_URL` | `http://127.0.0.1:8768` |
121
+ | `SEMRUSH_GALLETON_TOKEN_FILE` | `~/.local/share/semrush-cli/galleton/api.token` |
122
+ | `SEMRUSH_SESSION` | `semrush-cli` |
123
+ | `SEMRUSH_BROWSER_EXECUTABLE` | Patchright's Chromium |
124
+
125
+ The setup/daemon commands manage only the default CLI-owned daemon. An external
126
+ daemon must support the documented static-provider extension. Never expose its
127
+ API token or credential directory.
128
+
129
+ ## Limits
130
+
131
+ Semrush account permissions, quotas and product access apply. A read-only report
132
+ can consume an allowance. Internal web endpoints may change. Provider caps remain
133
+ visible; there is no unlimited-export claim. Galleton encrypts at rest but its key
134
+ and vault share a private local directory. Other processes running as your OS user
135
+ are within the same trust boundary.
136
+
137
+ ## Development
138
+
139
+ ```sh
140
+ npm ci
141
+ npm run check
142
+ npm --prefix docs-site ci
143
+ npm --prefix docs-site run build
144
+ ```
145
+
146
+ Development builds require Go 1.23+ and `tar`/`patch` to build the pinned Galleton
147
+ daemon with its storage-only extension. The build runs the upstream core suite
148
+ with the extension's integration test. Published users do not need Go.
149
+
150
+ Tests compare all 48 request shapes against the Python reference, use real
151
+ Chromium and Galleton with a local HTTP fixture, and exercise the built executable.
152
+ They never require a Semrush account. See [verification](docs/verification.md) for
153
+ the separate live-account evidence.
154
+
155
+ ## Uninstall
156
+
157
+ ```sh
158
+ semrush-cli auth logout
159
+ semrush-cli daemon stop
160
+ npm uninstall -g semrush-cli
161
+ ```
162
+
163
+ The private vault and browser cache remain on disk. Remove them only after
164
+ checking they contain nothing you need.
165
+
166
+ MIT. Independent project, not affiliated with Semrush.
@@ -0,0 +1,19 @@
1
+ # Third-party notices
2
+
3
+ ## Galleton
4
+
5
+ The TypeScript SDK and bundled daemon derive from Microck/galleton, commit
6
+ `efe7b742536d6c118c37a269bf626c5927e2a1b9`, under MIT.
7
+ The original license is included at `src/vendor/GALLETON-LICENSE`.
8
+ The SDK source is unchanged. The daemon includes the documented storage-only
9
+ provider patch at `patches/galleton-static-provider.patch` in the source repository.
10
+
11
+ ## Semrush web report contracts
12
+
13
+ The report catalog, schemas and request-shape fixtures derive from
14
+ Microck/semrush-web-mcp, commit `764d102f`, under MIT.
15
+ Copyright (c) 2026 Microck. See the repository LICENSE.
16
+
17
+ All package dependencies retain their own licenses in their installed packages.
18
+ This project is independent of Semrush and does not include Semrush trademarks
19
+ or credentials in its distribution.
Binary file
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>semrush-cli</title><path d="m9 19 12 13L9 45" fill="none" stroke="#fff" stroke-width="7" stroke-linejoin="miter"/><path fill="#ff8a3d" d="M27 36h7v12h-7zm11-9h7v21h-7zm11-11h7v32h-7z"/></svg>
@@ -0,0 +1,82 @@
1
+ import { n as SessionError, r as UsageError } from "./errors-BhvanypE.js";
2
+ import { i as Sessions, n as origin } from "./session-layer-BUU0ZG9M.js";
3
+ import "./browser-service-BYYE-vSm.js";
4
+ import { t as readBootstrap } from "./browser-layer-D6mEyv9Y.js";
5
+ import { Config, Effect, Option } from "effect";
6
+ import { chromium } from "patchright";
7
+
8
+ //#region src/auth.ts
9
+ const authFailed = () => new SessionError({ message: "Could not import a signed-in Semrush session. Check the browser, complete sign-in, and retry. No credentials were logged." });
10
+ const authCall = (run) => Effect.tryPromise({
11
+ try: run,
12
+ catch: authFailed
13
+ });
14
+ const saveContext = Effect.fn("Auth.saveContext")(function* (context) {
15
+ const sessions = yield* Sessions;
16
+ const page = yield* Effect.acquireRelease(authCall(() => context.newPage()), (page$1) => Effect.promise(() => page$1.close()));
17
+ yield* authCall(() => page.goto(origin + "/robots.txt", {
18
+ waitUntil: "domcontentloaded",
19
+ timeout: 45e3
20
+ }));
21
+ const html = yield* authCall(() => page.evaluate(async () => {
22
+ const response = await fetch("/analytics/overview/?db=us", {
23
+ credentials: "include",
24
+ signal: AbortSignal.timeout(45e3)
25
+ });
26
+ return response.ok ? response.text() : "";
27
+ }));
28
+ yield* Effect.try({
29
+ try: () => readBootstrap(html),
30
+ catch: authFailed
31
+ });
32
+ const cookies = yield* authCall(() => context.cookies(origin));
33
+ if (!cookies.length) return yield* Effect.fail(authFailed());
34
+ return yield* sessions.save({
35
+ provider: "semrush",
36
+ cookie_origin: origin,
37
+ cookie_header: cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ")
38
+ });
39
+ });
40
+ const importBrowser = Effect.fn("Auth.import")(function* (url) {
41
+ const valid = yield* Effect.try({
42
+ try: () => {
43
+ const parsed = new URL(url);
44
+ if (parsed.protocol !== "http:" || !["127.0.0.1", "[::1]"].includes(parsed.hostname) || parsed.username || parsed.password) throw new Error("Not loopback");
45
+ return parsed.origin;
46
+ },
47
+ catch: () => new UsageError({ message: "CDP must be a literal loopback HTTP URL. Forward remote browsers over SSH first." })
48
+ });
49
+ const browser = yield* authCall(() => chromium.connectOverCDP(valid, { timeout: 3e4 }));
50
+ yield* Effect.addFinalizer(() => Effect.promise(() => browser.close()));
51
+ const context = browser.contexts()[0];
52
+ if (!context) return yield* Effect.fail(authFailed());
53
+ return {
54
+ logged_in: true,
55
+ source: "galleton",
56
+ session: (yield* saveContext(context)).id
57
+ };
58
+ });
59
+ const login = Effect.fn("Auth.login")(function* () {
60
+ const executable = yield* Config.option(Config.String("SEMRUSH_BROWSER_EXECUTABLE"));
61
+ const browser = yield* Effect.acquireRelease(authCall(() => chromium.launch({
62
+ headless: false,
63
+ executablePath: Option.getOrUndefined(executable)
64
+ })), (browser$1) => Effect.promise(() => browser$1.close()));
65
+ const context = yield* authCall(() => browser.newContext({ userAgent: "OpenAI File Downloader, XaiImageApiFetch/1.0" }));
66
+ const page = yield* authCall(() => context.newPage());
67
+ yield* authCall(() => page.goto(origin + "/login/", {
68
+ waitUntil: "domcontentloaded",
69
+ timeout: 45e3
70
+ }));
71
+ yield* Effect.sync(() => process.stderr.write("Sign into Semrush in the browser. Waiting up to 5 minutes.\n"));
72
+ yield* authCall(() => page.waitForURL((url) => url.hostname === "www.semrush.com" && !url.pathname.startsWith("/login") && !url.pathname.startsWith("/signup"), { timeout: 3e5 }));
73
+ return {
74
+ logged_in: true,
75
+ source: "galleton",
76
+ session: (yield* saveContext(context)).id
77
+ };
78
+ });
79
+
80
+ //#endregion
81
+ export { importBrowser, login };
82
+ //# sourceMappingURL=auth-BcUPx0Nk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-BcUPx0Nk.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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"}
@@ -0,0 +1,269 @@
1
+ import { i as reportParameters, n as SessionError, t as ReportError } from "./errors-BhvanypE.js";
2
+ import { i as Sessions, n as origin } from "./session-layer-BUU0ZG9M.js";
3
+ import { t as Semrush } from "./browser-service-BYYE-vSm.js";
4
+ import { Config, Effect, Layer, Option, Predicate, Schema, Semaphore } from "effect";
5
+ import { chromium } from "patchright";
6
+ import { randomUUID } from "node:crypto";
7
+ import JSONbig from "json-bigint";
8
+
9
+ //#region src/browser-layer.ts
10
+ const transportPaths = new Map([
11
+ ["domain", "/dpa/rpc"],
12
+ ["direct", "/dpa/rpc"],
13
+ ["magic", "/kmtgw/v2/webapi"],
14
+ ["keyword", "/kwogw/v2/webapi"]
15
+ ]);
16
+ const json = JSONbig({
17
+ storeAsString: true,
18
+ protoAction: "error",
19
+ constructorAction: "error"
20
+ });
21
+ 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 browserCall = (run) => Effect.tryPromise({
23
+ try: run,
24
+ catch: safeBrowserError
25
+ });
26
+ const objectValue = (value) => Predicate.isObject(value) && !Array.isArray(value);
27
+ /** JSON never passes through the browser's number parser, preserving 64-bit IDs. */
28
+ function parseWire(text) {
29
+ return Schema.decodeUnknownSync(Schema.Json)(json.parse(text));
30
+ }
31
+ /** Remove credential fields and echoed secrets, including nested provider errors. */
32
+ function redact(value, secrets) {
33
+ if (Array.isArray(value)) return value.map((item) => redact(item, secrets));
34
+ 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)]));
35
+ if (Predicate.isString(value)) return secrets.filter((secret) => secret.length > 0).reduce((text, secret) => text.replaceAll(secret, "[redacted]"), value);
36
+ return value;
37
+ }
38
+ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(function* () {
39
+ const sessions = yield* Sessions;
40
+ const executable = yield* Config.option(Config.String("SEMRUSH_BROWSER_EXECUTABLE"));
41
+ const headers = yield* sessions.headers();
42
+ const browser = yield* Effect.acquireRelease(browserCall(() => chromium.launch({
43
+ headless: true,
44
+ executablePath: Option.getOrUndefined(executable)
45
+ })), (browser$1) => Effect.promise(() => browser$1.close()));
46
+ const context = yield* browserCall(() => browser.newContext({ userAgent: "OpenAI File Downloader, XaiImageApiFetch/1.0" }));
47
+ const cookieHeader = headers.Cookie ?? headers.cookie ?? "";
48
+ yield* browserCall(() => context.addCookies(cookieHeader.split(";").filter((cookie) => cookie.includes("=")).map((cookie) => {
49
+ const separator = cookie.indexOf("=");
50
+ return {
51
+ name: cookie.slice(0, separator).trim(),
52
+ value: cookie.slice(separator + 1).trim(),
53
+ url: browserOrigin,
54
+ httpOnly: true,
55
+ secure: browserOrigin.startsWith("https:"),
56
+ sameSite: "Lax"
57
+ };
58
+ })));
59
+ const page = yield* browserCall(() => context.newPage());
60
+ yield* browserCall(() => page.goto(browserOrigin + "/robots.txt", {
61
+ waitUntil: "domcontentloaded",
62
+ timeout: 45e3
63
+ }));
64
+ const bootstrap = yield* browserCall(() => page.evaluate(async () => {
65
+ const response = await fetch("/analytics/overview/?db=us", {
66
+ credentials: "include",
67
+ signal: AbortSignal.timeout(45e3)
68
+ });
69
+ return {
70
+ status: response.status,
71
+ text: response.ok ? await response.text() : ""
72
+ };
73
+ }));
74
+ const auth = yield* Effect.try({
75
+ try: () => readBootstrap(bootstrap.text),
76
+ catch: () => new SessionError({ message: "Semrush sign-in metadata unavailable. Run semrush-cli auth login or import a signed-in browser." })
77
+ });
78
+ const semaphore = yield* Semaphore.make(1);
79
+ let spectrumToken = "";
80
+ const request = Effect.fn("Semrush.request")(function* (path, payload, get = false, extraHeaders = {}) {
81
+ const wire = yield* browserCall(() => page.evaluate(async ({ path: path$1, encodedPayload, get: get$1, extraHeaders: extraHeaders$1 }) => {
82
+ const payload$1 = JSON.parse(encodedPayload);
83
+ const query = new URLSearchParams();
84
+ if (get$1) for (const [key, value] of Object.entries(payload$1)) for (const item of Array.isArray(value) ? value : [value]) query.append(key, String(item));
85
+ const options = {
86
+ method: get$1 ? "GET" : "POST",
87
+ credentials: "include",
88
+ headers: extraHeaders$1,
89
+ signal: AbortSignal.timeout(45e3)
90
+ };
91
+ if (!get$1) {
92
+ options.headers = {
93
+ ...extraHeaders$1,
94
+ "Content-Type": "application/json"
95
+ };
96
+ options.body = JSON.stringify(payload$1);
97
+ }
98
+ const response = await fetch(get$1 ? path$1 + "?" + query : path$1, options);
99
+ return {
100
+ status: response.status,
101
+ text: response.ok ? await response.text() : ""
102
+ };
103
+ }, {
104
+ path,
105
+ encodedPayload: JSON.stringify(payload),
106
+ get,
107
+ extraHeaders
108
+ }));
109
+ if (wire.status !== 200) return yield* Effect.fail(new ReportError({ message: `Semrush report returned HTTP ${wire.status}. Check your session and account permissions.` }));
110
+ const body = yield* Effect.try({
111
+ try: () => parseWire(wire.text),
112
+ catch: () => new ReportError({ message: "Semrush returned a page or challenge instead of JSON. Sign in again or retry later." })
113
+ });
114
+ 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, [
115
+ auth.apiKey,
116
+ spectrumToken,
117
+ spectrumToken.replace(/^Bearer /, "")
118
+ ])).slice(0, 500)}` }));
119
+ return body;
120
+ });
121
+ const rpc = Effect.fn("Semrush.rpc")(function* (method, params, transport = "keyword", webReport = "domain.overview") {
122
+ const payload = transport === "domain" ? {
123
+ request_id: randomUUID(),
124
+ report: webReport,
125
+ args: params,
126
+ ...auth
127
+ } : transport === "direct" ? {
128
+ ...params,
129
+ ...auth
130
+ } : params;
131
+ const body = yield* request(transportPaths.get(transport) ?? "/kwogw/v2/webapi", {
132
+ id: 1,
133
+ jsonrpc: "2.0",
134
+ method,
135
+ params: payload
136
+ });
137
+ if (!objectValue(body) || !("result" in body)) return yield* Effect.fail(new ReportError({ message: `Semrush ${method}: missing report result.` }));
138
+ return body.result ?? null;
139
+ });
140
+ const report = Effect.fn("Semrush.report")(function* ({ name, definition, query }) {
141
+ if (!(yield* browserCall(() => page.evaluate(async () => {
142
+ const response = await fetch("/home/", {
143
+ method: "HEAD",
144
+ credentials: "include",
145
+ cache: "no-store",
146
+ signal: AbortSignal.timeout(15e3)
147
+ });
148
+ return response.ok && new URL(response.url).pathname === "/home/";
149
+ })))) return yield* Effect.fail(new SessionError({ message: "Semrush session expired. Run semrush-cli auth login or import a signed-in browser." }));
150
+ const params = reportParameters(definition, query);
151
+ const family = definition.transport;
152
+ let reportData;
153
+ if ([
154
+ "keyword",
155
+ "domain",
156
+ "direct",
157
+ "magic"
158
+ ].includes(family)) reportData = yield* rpc(definition.method, params, family, definition.web_report);
159
+ else {
160
+ const extra = {};
161
+ if (family === "gap") {
162
+ if (!spectrumToken) spectrumToken = yield* browserCall(() => observeSpectrum(page, browserOrigin));
163
+ Object.assign(extra, {
164
+ authorization: spectrumToken,
165
+ "x-report": "gap.keywords",
166
+ "x-request-id": randomUUID()
167
+ });
168
+ }
169
+ let path = definition.method;
170
+ if (family.startsWith("backlink")) {
171
+ path = "/analytics/backlinks/webapi2/";
172
+ params.key = auth.apiKey;
173
+ if (family === "backlink_overview") {
174
+ path += "overview/" + definition.method;
175
+ extra["x-request-id"] = randomUUID();
176
+ yield* request("/analytics/backlinks/webapi2/overview/init", params, true, extra);
177
+ }
178
+ } else if (path.startsWith("/siteaudit/")) params.key = auth.apiKey;
179
+ reportData = yield* request(path, params, ![
180
+ "post",
181
+ "gap",
182
+ "projects"
183
+ ].includes(family), extra);
184
+ }
185
+ if (definition.total_method) {
186
+ const totalParams = name === "domain_position_changes" ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "display")) : params;
187
+ const total = yield* rpc(definition.total_method, totalParams, "domain", definition.web_report);
188
+ reportData = {
189
+ rows: reportData,
190
+ total,
191
+ page: query.page ?? null,
192
+ page_size: query.page_size ?? null
193
+ };
194
+ }
195
+ if (name === "keyword_overview") {
196
+ const rows = objectValue(reportData) ? reportData.keywords : void 0;
197
+ reportData = Array.isArray(rows) ? rows.find((item) => objectValue(item) && item.database === query.database) : void 0;
198
+ if (!reportData) return yield* Effect.fail(new ReportError({ message: "No keyword data for the selected database." }));
199
+ } else if (name === "domain_overview") {
200
+ const search = Array.isArray(reportData) ? reportData.find((item) => objectValue(item) && item.database === query.database) : void 0;
201
+ if (!search) return yield* Effect.fail(new ReportError({ message: "No domain data for the selected database." }));
202
+ reportData = {
203
+ search,
204
+ backlinks: yield* rpc("backlinks.Summary", {
205
+ searchItem: query.domain ?? null,
206
+ searchType: "domain"
207
+ }, "domain")
208
+ };
209
+ }
210
+ if (Predicate.isNumber(query.limit) && Array.isArray(reportData)) reportData = reportData.slice(0, query.limit);
211
+ yield* captureCookies(context, sessions.capture, browserOrigin);
212
+ return {
213
+ authenticated: true,
214
+ report: name,
215
+ data: redact(reportData ?? null, [
216
+ auth.apiKey,
217
+ spectrumToken,
218
+ spectrumToken.replace(/^Bearer /, "")
219
+ ])
220
+ };
221
+ });
222
+ return Semrush.of({ report: (request$1) => semaphore.withPermits(1)(report(request$1)) });
223
+ }));
224
+ const semrushLive = makeSemrushLayer(origin);
225
+ function readBootstrap(html) {
226
+ const match = /window\.sm2\.user\s*=\s*/.exec(html);
227
+ if (!match) throw new Error("No bootstrap");
228
+ const start = match.index + match[0].length;
229
+ let depth = 0, quoted = false, escaped = false;
230
+ for (let index = start; index < html.length; index++) {
231
+ const char = html[index];
232
+ if (quoted) {
233
+ if (escaped) escaped = false;
234
+ else if (char === "\\") escaped = true;
235
+ else if (char === "\"") quoted = false;
236
+ } else if (char === "\"") quoted = true;
237
+ else if (char === "{") depth++;
238
+ else if (char === "}" && --depth === 0) {
239
+ const user = parseWire(html.slice(start, index + 1));
240
+ if (objectValue(user) && Predicate.isString(user.api_key) && (Predicate.isString(user.id) || Predicate.isNumber(user.id))) return {
241
+ apiKey: user.api_key,
242
+ userId: user.id
243
+ };
244
+ break;
245
+ }
246
+ }
247
+ throw new Error("Invalid bootstrap");
248
+ }
249
+ async function observeSpectrum(page, browserOrigin) {
250
+ try {
251
+ const [observed] = await Promise.all([page.waitForRequest((request) => request.url().startsWith(browserOrigin + "/spectrum/") && Boolean(request.headers().authorization), { timeout: 2e4 }), page.goto(browserOrigin + "/analytics/keywordgap/?db=us", {
252
+ waitUntil: "domcontentloaded",
253
+ timeout: 45e3
254
+ })]);
255
+ return observed.headers().authorization ?? "";
256
+ } finally {
257
+ await page.goto(browserOrigin + "/robots.txt", {
258
+ waitUntil: "domcontentloaded",
259
+ timeout: 45e3
260
+ });
261
+ }
262
+ }
263
+ const captureCookies = Effect.fn("Semrush.captureCookies")(function* (context, capture, cookieOrigin) {
264
+ yield* capture((yield* browserCall(() => context.cookies(cookieOrigin))).map((cookie) => `${cookie.name}=${cookie.value}; Path=${cookie.path}${cookieOrigin.startsWith("https:") ? "; Secure" : ""}${cookie.httpOnly ? "; HttpOnly" : ""}${cookie.expires > 0 ? "; Expires=" + (/* @__PURE__ */ new Date(cookie.expires * 1e3)).toUTCString() : ""}`));
265
+ });
266
+
267
+ //#endregion
268
+ export { semrushLive as n, readBootstrap as t };
269
+ //# sourceMappingURL=browser-layer-D6mEyv9Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-layer-D6mEyv9Y.js","names":["browser","payload: Schema.JsonObject","get","payload","options: RequestInit","extraHeaders","path","reportData: Schema.Json | undefined","extra: Record<string, string>","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, 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']]);\nconst json = JSONbig({ storeAsString: true, protoAction: 'error', constructorAction: 'error' });\nconst safeBrowserError = () => new ReportError({ message: 'Semrush browser request failed. Run semrush-cli doctor. If your session expired, sign in again with semrush-cli 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: response.ok ? 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-cli 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, get = false, extraHeaders: Record<string, string> = {}) {\n const wire = yield* browserCall(() => page.evaluate(async ({ path, encodedPayload, get, extraHeaders }) => {\n const payload: Schema.JsonObject = JSON.parse(encodedPayload);\n const query = new URLSearchParams();\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: get ? 'GET' : 'POST', credentials: 'include', headers: extraHeaders, signal: AbortSignal.timeout(45000) };\n if (!get) { options.headers = { ...extraHeaders, 'Content-Type': 'application/json' }; options.body = JSON.stringify(payload); }\n const response = await fetch(get ? path + '?' + query : path, options);\n return { status: response.status, text: response.ok ? await response.text() : '' };\n }, { path, encodedPayload: JSON.stringify(payload), get, extraHeaders }));\n if (wire.status !== 200) return yield* Effect.fail(new ReportError({ message: `Semrush report returned HTTP ${wire.status}. Check your session and account permissions.` }));\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 || ('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 body = yield* request(transportPaths.get(transport) ?? '/kwogw/v2/webapi', { id: 1, jsonrpc: '2.0', method, params: payload });\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 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-cli auth login or import a signed-in browser.' }));\n const params = reportParameters(definition, query);\n const family = definition.transport;\n let reportData: Schema.Json | undefined;\n if (['keyword', 'domain', 'direct', 'magic'].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, true, extra);\n }\n } else if (path.startsWith('/siteaudit/')) params.key = auth.apiKey;\n reportData = yield* request(path, params, !['post', 'gap', 'projects'].includes(family), 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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"}
@@ -0,0 +1,9 @@
1
+ import { Context } from "effect";
2
+
3
+ //#region src/browser-service.ts
4
+ /** One serialized browser context per runtime. Report callers never handle cookies or pages. */
5
+ var Semrush = class extends Context.Service()("semrush/Semrush") {};
6
+
7
+ //#endregion
8
+ export { Semrush as t };
9
+ //# sourceMappingURL=browser-service-BYYE-vSm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-service-BYYE-vSm.js","names":[],"sources":["../src/browser-service.ts"],"sourcesContent":["import { Context, Effect } from 'effect';\nimport type { ReportRequest } from './validation.js';\nimport type { ReportError, SessionError, UsageError } from './errors.js';\n\n/** One serialized browser context per runtime. Report callers never handle cookies or pages. */\nexport class Semrush extends Context.Service<Semrush, {\n readonly report: (request: ReportRequest) => Effect.Effect<{ authenticated: true; report: string; data: unknown }, ReportError | SessionError | UsageError>;\n}>()('semrush/Semrush') {}\n"],"mappings":";;;;AAKA,IAAa,UAAb,cAA6B,QAAQ,SAEjC,CAAC,kBAAkB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { t as Semrush } from "./browser-service-BYYE-vSm.js";
2
+
3
+ export { Semrush };