semrush-cli 0.1.1 → 0.3.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,16 +1,31 @@
1
- <p align="center"><img src="assets/logo.svg" width="100" alt="semrush-cli"></p>
1
+ <p align="center">
2
+ <img src="assets/logo.svg" width="220" alt="semrush-cli">
3
+ </p>
2
4
  <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>
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>
10
+ </p>
5
11
 
6
- The executable is `semrush-cli`. It can coexist with other tools named `semrush`.
12
+ ---
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.
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)
19
+
20
+ ## why
7
21
 
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.
22
+ 81 operations with the same input schemas in the CLI and MCP. JSON output for
23
+ scripts, account totals and pagination, project management, and encrypted session storage.
24
+ no Semrush developer API key or Python runtime required.
12
25
 
13
- ## Install
26
+ The executable is `semrush-cli`. It can coexist with other tools named `semrush`.
27
+
28
+ ## quickstart
14
29
 
15
30
  Requires Node.js 22+. Includes Galleton binaries for Linux x64/arm64,
16
31
  macOS x64/arm64 and Windows x64.
@@ -39,7 +54,43 @@ supports the browser managed by [semrush-web-mcp](https://github.com/Microck/sem
39
54
  Import does not stop or change that browser. Keep CDP private; forward it over
40
55
  SSH when necessary.
41
56
 
42
- ## Research
57
+ ## projects
58
+
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
68
+ ```
69
+
70
+ Project commands use the current web UI's folder IDs. Creation does not start
71
+ crawls or share the project. Every write requires `--confirm`; `--dry-run` prints
72
+ the request without authentication or network access. SEO project creation and
73
+ automatic Site Audit/Position Tracking setup are separate, explicit commands.
74
+ See [projects](https://semrush.micr.dev/docs/projects) and
75
+ [web UI coverage](https://semrush.micr.dev/docs/coverage).
76
+
77
+ ## keyword lists
78
+
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
86
+ ```
87
+
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).
92
+
93
+ ## commands
43
94
 
44
95
  ```sh
45
96
  semrush-cli keyword-overview --keyword 'email marketing' --json
@@ -62,7 +113,9 @@ semrush-cli domain-keywords --help
62
113
  | Gaps | Keyword overlaps and gaps, backlink prospects and profile comparisons |
63
114
  | Backlinks | Links, referring domains, anchors, indexed pages, outbound domains, history and authority |
64
115
  | AI visibility | Visibility, mentions, citation sources and country distribution |
65
- | Account | Limits, permissions, databases, snapshots, projects, folders, saved lists and audit campaigns |
116
+ | Keyword lists | Create, read, rename/delete, import, metrics, tags, deletion/undo, clustering and structured strategy generation |
117
+ | Projects | Create, inspect, rename, delete, leave, tags, pinning, toolkit status, dashboard metrics and explicit SEO setup |
118
+ | Account | Limits, permissions, databases, snapshots, folders, saved lists and audit campaigns |
66
119
 
67
120
  All successful reports use `{authenticated: true, report, data}`. Domain ranking
68
121
  and page reports include provider totals. Large integers remain exact as decimal
@@ -70,7 +123,7 @@ strings. Errors go to stderr with a nonzero exit. `--json` works before or after
70
123
  the command. Objects and arrays use JSON flags; booleans support explicit `false`.
71
124
  See the [full command reference](https://semrush.micr.dev/docs/reports).
72
125
 
73
- ## MCP
126
+ ## mcp
74
127
 
75
128
  Complete setup and authentication, then configure your MCP client:
76
129
 
@@ -86,12 +139,14 @@ Complete setup and authentication, then configure your MCP client:
86
139
  ```
87
140
 
88
141
  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,
142
+ PATH. `npx -y semrush-cli@0.3.0 mcp` also works. MCP tool names use underscores,
90
143
  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.
144
+ match the CLI. The 62 read tools carry read-only annotations. The 19 write tools require
145
+ `confirm:true`, or accept `dry_run:true` to preview the request. Delete and leave
146
+ also carry destructive annotations. Login, arbitrary URLs and JavaScript
147
+ execution are not MCP tools.
93
148
 
94
- ## Sessions
149
+ ## sessions
95
150
 
96
151
  ```sh
97
152
  semrush-cli auth status --check --json
@@ -126,7 +181,7 @@ The setup/daemon commands manage only the default CLI-owned daemon. An external
126
181
  daemon must support the documented static-provider extension. Never expose its
127
182
  API token or credential directory.
128
183
 
129
- ## Limits
184
+ ## limits
130
185
 
131
186
  Semrush account permissions, quotas and product access apply. A read-only report
132
187
  can consume an allowance. Internal web endpoints may change. Provider caps remain
@@ -134,7 +189,7 @@ visible; there is no unlimited-export claim. Galleton encrypts at rest but its k
134
189
  and vault share a private local directory. Other processes running as your OS user
135
190
  are within the same trust boundary.
136
191
 
137
- ## Development
192
+ ## development
138
193
 
139
194
  ```sh
140
195
  npm ci
@@ -152,7 +207,7 @@ Chromium and Galleton with a local HTTP fixture, and exercise the built executab
152
207
  They never require a Semrush account. See [verification](docs/verification.md) for
153
208
  the separate live-account evidence.
154
209
 
155
- ## Uninstall
210
+ ## uninstall
156
211
 
157
212
  ```sh
158
213
  semrush-cli auth logout
@@ -15,5 +15,7 @@ Microck/semrush-web-mcp, commit `764d102f`, under MIT.
15
15
  Copyright (c) 2026 Microck. See the repository LICENSE.
16
16
 
17
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.
18
+ The modified logo derives from the official Semrush SVG icon in
19
+ https://brand.semrush.com/Semrush%20Logo.zip. Semrush owns the underlying mark.
20
+ This project is independent of Semrush. Its distribution contains no account
21
+ credentials.
package/assets/logo.png CHANGED
Binary file
package/assets/logo.svg CHANGED
@@ -1 +1,10 @@
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>
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)"/>
10
+ </svg>
@@ -1,7 +1,7 @@
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";
1
+ import { n as SessionError, r as UsageError } from "./errors-CU9T4acd.js";
2
+ import { i as Sessions, n as origin } from "./session-layer-BDgTji_P.js";
3
3
  import "./browser-service-BYYE-vSm.js";
4
- import { t as readBootstrap } from "./browser-layer-D6mEyv9Y.js";
4
+ import { t as readBootstrap } from "./browser-layer-BJ4eQAUz.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-BcUPx0Nk.js.map
82
+ //# sourceMappingURL=auth-BZhlPkLq.js.map
@@ -1 +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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"}
1
+ {"version":3,"file":"auth-BZhlPkLq.js","names":["page","browser"],"sources":["../src/auth.ts"],"sourcesContent":["import { Config, Effect, Option } from 'effect';\nimport { chromium, type BrowserContext } from 'patchright';\nimport { Sessions } from './session-service.js';\nimport { origin } from './session-layer.js';\nimport { SessionError, UsageError } from './errors.js';\nimport { readBootstrap } from './browser-layer.js';\n\nconst authFailed = () => new SessionError({ message: 'Could not import a signed-in Semrush session. Check the browser, complete sign-in, and retry. No credentials were logged.' });\nconst authCall = <A>(run: () => Promise<A>) => Effect.tryPromise({ try: run, catch: authFailed });\n\nconst saveContext = Effect.fn('Auth.saveContext')(function* (context: BrowserContext) {\n const sessions = yield* Sessions;\n const page = yield* Effect.acquireRelease(authCall(() => context.newPage()), page => Effect.promise(() => page.close()));\n yield* authCall(() => page.goto(origin + '/robots.txt', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n const html = yield* authCall(() => page.evaluate(async () => {\n const response = await fetch('/analytics/overview/?db=us', { credentials: 'include', signal: AbortSignal.timeout(45000) });\n return response.ok ? response.text() : '';\n }));\n yield* Effect.try({ try: () => readBootstrap(html), catch: authFailed });\n const cookies = yield* authCall(() => context.cookies(origin));\n if (!cookies.length) return yield* Effect.fail(authFailed());\n // Bare cookies are intentionally bound to the single approved origin by Galleton.\n return yield* sessions.save({ provider: 'semrush', cookie_origin: origin, cookie_header: cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ') });\n});\n\nexport const importBrowser = Effect.fn('Auth.import')(function* (url: string) {\n const valid = yield* Effect.try({ try: () => {\n const parsed = new URL(url);\n if (parsed.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(parsed.hostname) || parsed.username || parsed.password) throw new Error('Not loopback');\n return parsed.origin;\n }, catch: () => new UsageError({ message: 'CDP must be a literal loopback HTTP URL. Forward remote browsers over SSH first.' }) });\n const browser = yield* authCall(() => chromium.connectOverCDP(valid, { timeout: 30000 }));\n // Closing a CDP connection disconnects this client and leaves the existing browser running.\n yield* Effect.addFinalizer(() => Effect.promise(() => browser.close()));\n const context = browser.contexts()[0];\n if (!context) return yield* Effect.fail(authFailed());\n const status = yield* saveContext(context);\n return { logged_in: true, source: 'galleton', session: status.id };\n});\n\nexport const login = Effect.fn('Auth.login')(function* () {\n const executable = yield* Config.option(Config.String('SEMRUSH_BROWSER_EXECUTABLE'));\n const browser = yield* Effect.acquireRelease(authCall(() => chromium.launch({ headless: false, executablePath: Option.getOrUndefined(executable) })), browser => Effect.promise(() => browser.close()));\n const context = yield* authCall(() => browser.newContext({ userAgent: 'OpenAI File Downloader, XaiImageApiFetch/1.0' }));\n const page = yield* authCall(() => context.newPage());\n yield* authCall(() => page.goto(origin + '/login/', { waitUntil: 'domcontentloaded', timeout: 45000 }));\n yield* Effect.sync(() => process.stderr.write('Sign into Semrush in the browser. Waiting up to 5 minutes.\\n'));\n yield* authCall(() => page.waitForURL(url => url.hostname === 'www.semrush.com' && !url.pathname.startsWith('/login') && !url.pathname.startsWith('/signup'), { timeout: 300000 }));\n const status = yield* saveContext(context);\n return { logged_in: true, source: 'galleton', session: status.id };\n});\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
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";
1
+ import { a as reportParameters, l as webRequest, n as SessionError, s as rpcBody, t as ReportError } from "./errors-CU9T4acd.js";
2
+ import { i as Sessions, n as origin } from "./session-layer-BDgTji_P.js";
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";
@@ -11,7 +11,8 @@ const transportPaths = new Map([
11
11
  ["domain", "/dpa/rpc"],
12
12
  ["direct", "/dpa/rpc"],
13
13
  ["magic", "/kmtgw/v2/webapi"],
14
- ["keyword", "/kwogw/v2/webapi"]
14
+ ["keyword", "/kwogw/v2/webapi"],
15
+ ["kwm", "/kwm/rpc"]
15
16
  ]);
16
17
  const json = JSONbig({
17
18
  storeAsString: true,
@@ -68,7 +69,7 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
68
69
  });
69
70
  return {
70
71
  status: response.status,
71
- text: response.ok ? await response.text() : ""
72
+ text: await response.text()
72
73
  };
73
74
  }));
74
75
  const auth = yield* Effect.try({
@@ -77,36 +78,48 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
77
78
  });
78
79
  const semaphore = yield* Semaphore.make(1);
79
80
  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 }) => {
81
+ const request = Effect.fn("Semrush.request")(function* (path, payload, method = "POST", extraHeaders = {}) {
82
+ const wire = yield* browserCall(() => page.evaluate(async ({ path: path$1, encodedPayload, method: method$1, extraHeaders: extraHeaders$1 }) => {
82
83
  const payload$1 = JSON.parse(encodedPayload);
83
84
  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 get = method$1 === "GET";
86
+ if (get) for (const [key, value] of Object.entries(payload$1)) for (const item of Array.isArray(value) ? value : [value]) query.append(key, String(item));
85
87
  const options = {
86
- method: get$1 ? "GET" : "POST",
88
+ method: method$1,
87
89
  credentials: "include",
88
90
  headers: extraHeaders$1,
89
91
  signal: AbortSignal.timeout(45e3)
90
92
  };
91
- if (!get$1) {
93
+ if (!get && method$1 !== "DELETE") {
92
94
  options.headers = {
93
95
  ...extraHeaders$1,
94
96
  "Content-Type": "application/json"
95
97
  };
96
98
  options.body = JSON.stringify(payload$1);
97
99
  }
98
- const response = await fetch(get$1 ? path$1 + "?" + query : path$1, options);
100
+ const response = await fetch(get && query.size > 0 ? path$1 + "?" + query : path$1, options);
99
101
  return {
100
102
  status: response.status,
101
- text: response.ok ? await response.text() : ""
103
+ text: await response.text()
102
104
  };
103
105
  }, {
104
106
  path,
105
107
  encodedPayload: JSON.stringify(payload),
106
- get,
108
+ method,
107
109
  extraHeaders
108
110
  }));
109
- if (wire.status !== 200) return yield* Effect.fail(new ReportError({ message: `Semrush report returned HTTP ${wire.status}. Check your session and account permissions.` }));
111
+ if (wire.status < 200 || wire.status >= 300) {
112
+ const detail = yield* Effect.try({
113
+ try: () => JSON.stringify(redact(parseWire(wire.text), [
114
+ auth.apiKey,
115
+ spectrumToken,
116
+ spectrumToken.replace(/^Bearer /, "")
117
+ ])).slice(0, 500),
118
+ catch: safeBrowserError
119
+ }).pipe(Effect.option);
120
+ return yield* Effect.fail(new ReportError({ message: `Semrush request returned HTTP ${wire.status}.${Option.isSome(detail) ? " " + detail.value : " Check your session and account permissions."}` }));
121
+ }
122
+ if (wire.status === 204 && !wire.text) return null;
110
123
  const body = yield* Effect.try({
111
124
  try: () => parseWire(wire.text),
112
125
  catch: () => new ReportError({ message: "Semrush returned a page or challenge instead of JSON. Sign in again or retry later." })
@@ -128,12 +141,21 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
128
141
  ...params,
129
142
  ...auth
130
143
  } : 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
- });
144
+ const rpcParams = transport === "kwm" ? {
145
+ ...payload,
146
+ ...method.startsWith("user.") ? {
147
+ api_key: auth.apiKey,
148
+ user_id: auth.userId
149
+ } : {
150
+ APIKey: auth.apiKey,
151
+ UserID: auth.userId
152
+ }
153
+ } : payload;
154
+ const extra = transport === "kwm" ? {
155
+ "api-key": auth.apiKey,
156
+ "user-id": String(auth.userId)
157
+ } : {};
158
+ const body = yield* request(transportPaths.get(transport) ?? "/kwogw/v2/webapi", rpcBody(method, rpcParams), "POST", extra);
137
159
  if (!objectValue(body) || !("result" in body)) return yield* Effect.fail(new ReportError({ message: `Semrush ${method}: missing report result.` }));
138
160
  return body.result ?? null;
139
161
  });
@@ -147,14 +169,23 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
147
169
  });
148
170
  return response.ok && new URL(response.url).pathname === "/home/";
149
171
  })))) 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
172
  const family = definition.transport;
173
+ const planned = family === "project_web" ? webRequest(definition, query) : void 0;
174
+ const params = planned?.body ?? reportParameters(definition, query);
152
175
  let reportData;
153
- if ([
176
+ if (planned) {
177
+ if (name === "project_update" && query.domain !== void 0) {
178
+ const current = yield* request("/apis/v4-raw/folders/api/v0/folders/folder", { folder_id: query.folder_id ?? null }, "GET");
179
+ if (!objectValue(current) || !objectValue(current.folder)) return yield* Effect.fail(new ReportError({ message: "Project metadata unavailable. Inspect the project before adding a website." }));
180
+ if (Predicate.isString(current.folder.domain) && current.folder.domain.length > 0) return yield* Effect.fail(new ReportError({ message: "This project already has a website. The Semrush web UI only allows adding a website when none is set." }));
181
+ }
182
+ reportData = yield* request(planned.path, planned.body, planned.method);
183
+ } else if ([
154
184
  "keyword",
155
185
  "domain",
156
186
  "direct",
157
- "magic"
187
+ "magic",
188
+ "kwm"
158
189
  ].includes(family)) reportData = yield* rpc(definition.method, params, family, definition.web_report);
159
190
  else {
160
191
  const extra = {};
@@ -173,14 +204,14 @@ const makeSemrushLayer = (browserOrigin) => Layer.effect(Semrush, Effect.gen(fun
173
204
  if (family === "backlink_overview") {
174
205
  path += "overview/" + definition.method;
175
206
  extra["x-request-id"] = randomUUID();
176
- yield* request("/analytics/backlinks/webapi2/overview/init", params, true, extra);
207
+ yield* request("/analytics/backlinks/webapi2/overview/init", params, "GET", extra);
177
208
  }
178
209
  } else if (path.startsWith("/siteaudit/")) params.key = auth.apiKey;
179
- reportData = yield* request(path, params, ![
210
+ reportData = yield* request(path, params, [
180
211
  "post",
181
212
  "gap",
182
213
  "projects"
183
- ].includes(family), extra);
214
+ ].includes(family) ? "POST" : "GET", extra);
184
215
  }
185
216
  if (definition.total_method) {
186
217
  const totalParams = name === "domain_position_changes" ? Object.fromEntries(Object.entries(params).filter(([key]) => key !== "display")) : params;
@@ -266,4 +297,4 @@ const captureCookies = Effect.fn("Semrush.captureCookies")(function* (context, c
266
297
 
267
298
  //#endregion
268
299
  export { semrushLive as n, readBootstrap as t };
269
- //# sourceMappingURL=browser-layer-D6mEyv9Y.js.map
300
+ //# sourceMappingURL=browser-layer-BJ4eQAUz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-layer-BJ4eQAUz.js","names":["browser","payload: Schema.JsonObject","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-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: 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, method = 'POST', extraHeaders: Record<string, string> = {}) {\n const wire = yield* browserCall(() => page.evaluate(async ({ path, encodedPayload, method, extraHeaders }) => {\n const payload: Schema.JsonObject = 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') { options.headers = { ...extraHeaders, 'Content-Type': 'application/json' }; options.body = JSON.stringify(payload); }\n const response = await fetch(get && query.size > 0 ? path + '?' + 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 || ('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 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 family = definition.transport;\n const planned = family === 'project_web' ? 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 reportData = yield* request(planned.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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"}
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as reports, n as SessionError, o as version } from "./errors-BhvanypE.js";
3
- import { i as Sessions, n as origin, r as sessionsLive, t as defaultStateDirectory } from "./session-layer-BUU0ZG9M.js";
2
+ import { c as version, i as previewMutation, n as SessionError, o as reports } from "./errors-CU9T4acd.js";
3
+ import { i as Sessions, n as origin, r as sessionsLive, t as defaultStateDirectory } from "./session-layer-BDgTji_P.js";
4
4
  import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
5
5
  import { Effect } from "effect";
6
6
  import { chmod, mkdir, open, writeFile } from "node:fs/promises";
@@ -137,11 +137,13 @@ async function execute(program, command) {
137
137
  }
138
138
  })), { signal: interruption.signal });
139
139
  }
140
- cli.command("reports").description("List all 48 read-only research reports").action((_options, command) => {
140
+ cli.command("reports").description("List research reports and project operations").action((_options, command) => {
141
141
  output(Object.entries(reports).map(([name, report]) => ({
142
142
  command: name.replaceAll("_", "-"),
143
143
  tool: name,
144
144
  description: report.description,
145
+ read_only: !report.mutation,
146
+ destructive: report.destructive,
145
147
  schema: report.schema
146
148
  })), command);
147
149
  });
@@ -173,7 +175,7 @@ for (const [name, definition] of Object.entries(reports)) {
173
175
  report.addOption(option);
174
176
  }
175
177
  report.action(async (_options, command) => {
176
- const { parseReport } = await import("./validation-BH5mq69L.js");
178
+ const { parseReport } = await import("./validation-D0km8JI3.js");
177
179
  const options = command.opts();
178
180
  const input = Object.fromEntries(Object.keys(schema.properties).map((key) => [key, options[key.replace(/_([a-z])/g, (_match, char) => char.toUpperCase())]]).filter(([, value]) => value !== void 0));
179
181
  const parsed = await Effect.runPromise(parseReport(name, input).pipe(Effect.match({
@@ -191,7 +193,12 @@ for (const [name, definition] of Object.entries(reports)) {
191
193
  process.exitCode = 2;
192
194
  return;
193
195
  }
194
- const { appLayer } = await import("./mcp-Cuzenjra.js");
196
+ const preview = previewMutation(parsed.value.name, parsed.value.definition, parsed.value.query);
197
+ if (preview) {
198
+ output(preview, command);
199
+ return;
200
+ }
201
+ const { appLayer } = await import("./mcp-BKmnk8Wq.js");
195
202
  const { Semrush } = await import("./browser-service-CL8YPaIT.js");
196
203
  await execute(Effect.gen(function* () {
197
204
  return yield* (yield* Semrush).report(parsed.value);
@@ -210,14 +217,14 @@ const daemon = cli.command("daemon").description("Control the CLI-owned Galleton
210
217
  daemon.command("start").description("Start the private daemon").action((_options, command) => execute(startDaemon(), command));
211
218
  daemon.command("stop").description("Gracefully stop the private daemon").action((_options, command) => execute(stopDaemon(), command));
212
219
  const auth = cli.command("auth").description("Store and inspect your Semrush session in Galleton");
213
- auth.command("import").description("Import a signed-in local Chromium context").requiredOption("--cdp-url <url>", "Loopback CDP URL, e.g. http://127.0.0.1:9234").action(async (options, command) => execute((await import("./auth-BcUPx0Nk.js")).importBrowser(options.cdpUrl).pipe(Effect.provide(sessionsLive)), command));
214
- auth.command("login").description("Sign in through an isolated visible Chromium window").action(async (_options, command) => execute((await import("./auth-BcUPx0Nk.js")).login().pipe(Effect.provide(sessionsLive)), command));
220
+ auth.command("import").description("Import a signed-in local Chromium context").requiredOption("--cdp-url <url>", "Loopback CDP URL, e.g. http://127.0.0.1:9234").action(async (options, command) => execute((await import("./auth-BZhlPkLq.js")).importBrowser(options.cdpUrl).pipe(Effect.provide(sessionsLive)), command));
221
+ auth.command("login").description("Sign in through an isolated visible Chromium window").action(async (_options, command) => execute((await import("./auth-BZhlPkLq.js")).login().pipe(Effect.provide(sessionsLive)), command));
215
222
  auth.command("status").description("Show session metadata without credentials").option("--check", "Verify the stored session against Semrush").action((options, command) => execute(Effect.gen(function* () {
216
223
  const status = yield* (yield* Sessions).status();
217
224
  if (options.check) {
218
- const { appLayer } = yield* Effect.promise(() => import("./mcp-Cuzenjra.js"));
225
+ const { appLayer } = yield* Effect.promise(() => import("./mcp-BKmnk8Wq.js"));
219
226
  const { Semrush } = yield* Effect.promise(() => import("./browser-service-CL8YPaIT.js"));
220
- const { parseReport } = yield* Effect.promise(() => import("./validation-BH5mq69L.js"));
227
+ const { parseReport } = yield* Effect.promise(() => import("./validation-D0km8JI3.js"));
221
228
  yield* Effect.gen(function* () {
222
229
  return yield* (yield* Semrush).report(yield* parseReport("account_status", {}));
223
230
  }).pipe(Effect.provide(appLayer));
@@ -237,9 +244,9 @@ auth.command("logout").description("Forget this CLI session from Galleton").acti
237
244
  };
238
245
  }).pipe(Effect.provide(sessionsLive)), command));
239
246
  cli.command("doctor").description("Check Galleton, browser and authenticated report access").action(async (_options, command) => {
240
- const { appLayer } = await import("./mcp-Cuzenjra.js");
247
+ const { appLayer } = await import("./mcp-BKmnk8Wq.js");
241
248
  const { Semrush } = await import("./browser-service-CL8YPaIT.js");
242
- const { parseReport } = await import("./validation-BH5mq69L.js");
249
+ const { parseReport } = await import("./validation-D0km8JI3.js");
243
250
  await execute(Effect.gen(function* () {
244
251
  yield* (yield* Semrush).report(yield* parseReport("account_status", {}));
245
252
  return {
@@ -250,7 +257,7 @@ cli.command("doctor").description("Check Galleton, browser and authenticated rep
250
257
  };
251
258
  }).pipe(Effect.provide(appLayer)), command);
252
259
  });
253
- cli.command("mcp").description("Run the read-only MCP server over stdio").action(async () => (await import("./mcp-Cuzenjra.js")).serveMcp());
260
+ cli.command("mcp").description("Run the MCP server over stdio").action(async () => (await import("./mcp-BKmnk8Wq.js")).serveMcp());
254
261
  process.once("SIGINT", () => {
255
262
  if (process.argv.includes("mcp")) return;
256
263
  process.exitCode = 130;