@siteoshq/cli 1.12.0 → 1.13.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,7 +1,7 @@
1
1
  # SiteOS CLI
2
2
 
3
3
  `@siteoshq/cli` exposes one `siteos` binary for Auth, common Projects, Pulse, Cookie, Forms,
4
- Search, Trace, SEO/GEO and Integrations. This source is version 1.11.0; source changes require a separate release to reach npm.
4
+ Search, Trace, SEO/GEO and Integrations. This source is version 1.13.0; source changes require a separate release to reach npm.
5
5
  Node.js 22 or newer is required.
6
6
 
7
7
  ## Install and authenticate
@@ -205,6 +205,10 @@ Research and GSC Insights use these commands with a matching server release:
205
205
  siteos seo research summary --json
206
206
  siteos seo research history --kind keywords --json
207
207
  siteos seo research show <run-id> --json
208
+ siteos seo research serp show <run-id> --keyword "website analytics" --json
209
+ siteos seo research serp ensure <run-id> --keyword "website analytics" --json
210
+ siteos seo research serp wait <run-id> --keyword "website analytics" --json
211
+ siteos seo research serp export <run-id> --keyword "website analytics" --format json --output ./serp-report.json --json
208
212
  siteos seo research plan --input ./research-request.json --json
209
213
  siteos seo research run --input ./research-request.json --idempotency-key <retry-key> --json
210
214
  siteos seo research wait <run-id> --json
@@ -219,6 +223,8 @@ Research supports keywords, Domain Overview (`domain`), rankings, backlinks, bra
219
223
  Domain Overview collects domain totals, up to 100 ranking keywords, pages and competitors for a market.
220
224
  `--kind domain` includes legacy `competitors` history; existing `competitors` inputs keep their original two-part plan.
221
225
  New request example: `{"kind":"domain","target":"example.com","country":"US","language":"en"}`.
226
+ SERP `ensure` collects at most one saved snapshot per returned keyword and Keyword Research report. Repeat calls reuse the saved job, including failures. SERP show/wait/export only read; market and device come from the parent report.
227
+
222
228
  Reading/planning/saving spends no research credits. Launch uses Organization credits and the same
223
229
  server admission as the interface. Keep the retry key after an uncertain response; no paid retries
224
230
  are automatic. Wait exit 3 means pending; exit 4 failed/cancelled. Partial evidence remains readable.
package/dist/cli.js CHANGED
@@ -10516,6 +10516,10 @@ var RESEARCH_HELP = `
10516
10516
  siteos seo research wait <run-id> [--timeout <seconds>] [--environment <slug>] [--json]
10517
10517
  siteos seo research cancel <run-id> [--environment <slug>] [--json]
10518
10518
  siteos seo research export <run-id> --format <json|csv> --output <new-file> [--environment <slug>] [--json]
10519
+ siteos seo research serp show <run-id> --keyword <word> [--environment <slug>] [--json]
10520
+ siteos seo research serp ensure <run-id> --keyword <word> [--environment <slug>] [--json]
10521
+ siteos seo research serp wait <run-id> --keyword <word> [--timeout <seconds>] [--environment <slug>] [--json]
10522
+ siteos seo research serp export <run-id> --keyword <word> --format <json|csv> --output <new-file> [--environment <slug>] [--json]
10519
10523
  siteos seo research saved list --kind <kind> [--environment <slug>] [--json]
10520
10524
  siteos seo research saved save --input <request.json> --name <name> [--id <saved-id>] [--environment <slug>] [--json]
10521
10525
  siteos seo research saved remove <saved-id> [--environment <slug>] [--json]
@@ -10529,6 +10533,7 @@ Markets: US, GB, ES, DE, FR, CA, AU. Languages: en, es, de, fr, pt, it, nl, ru.
10529
10533
  Platforms: chat_gpt, claude, gemini, perplexity. Brand lookup uses brandPlatform chat_gpt|google and brandMatch domain|brand.
10530
10534
  Plan validates the request and shows planned parts and available research credits without enqueueing work.
10531
10535
  Run consumes Organization research credits through the same worker as the interface. Preserve the retry key after an uncertain response.
10536
+ SERP ensure collects one saved organic snapshot for a keyword returned by the selected completed Keyword Research report. It uses research credits once; repeating the same report/word returns the saved job, including failures. Show/wait/export only read and never spend. New reports can collect fresh snapshots.
10532
10537
  History returns the latest 30 checks per kind; show/export can address older retained run IDs. Saved checks are limited to 100 per resource.
10533
10538
  Wait defaults to 120 seconds (maximum 900); exit 3 means still pending, exit 4 means failed or cancelled. Partial evidence stays accessible.
10534
10539
  Exports preserve scope, dates and partial state. CSV stores one dataset row as JSON per row, including failed parts; it never converts missing metrics to zero.
@@ -10569,6 +10574,7 @@ async function runSeoResearchCommand(options) {
10569
10574
  json: { type: "boolean" },
10570
10575
  environment: { type: "string" },
10571
10576
  kind: { type: "string" },
10577
+ keyword: { type: "string" },
10572
10578
  input: { type: "string" },
10573
10579
  name: { type: "string" },
10574
10580
  id: { type: "string" },
@@ -10578,9 +10584,13 @@ async function runSeoResearchCommand(options) {
10578
10584
  output: { type: "string" }
10579
10585
  }
10580
10586
  });
10581
- const action = positionals[0] === "saved" ? positionals.slice(0, 2).join(" ") : positionals[0] ?? "";
10582
- const id = positionals[action.startsWith("saved ") ? 2 : 1];
10587
+ const action = ["saved", "serp"].includes(positionals[0] ?? "") ? positionals.slice(0, 2).join(" ") : positionals[0] ?? "";
10588
+ const id = positionals[action.includes(" ") ? 2 : 1];
10583
10589
  const operations = {
10590
+ "serp show": { args: 3, flags: ["keyword"] },
10591
+ "serp ensure": { args: 3, flags: ["keyword"] },
10592
+ "serp wait": { args: 3, flags: ["keyword", "timeout"] },
10593
+ "serp export": { args: 3, flags: ["keyword", "format", "output"] },
10584
10594
  summary: { args: 1, flags: [] },
10585
10595
  status: { args: 1, flags: ["kind"] },
10586
10596
  history: { args: 1, flags: ["kind"] },
@@ -10601,6 +10611,10 @@ async function runSeoResearchCommand(options) {
10601
10611
  throw new Error(
10602
10612
  "Invalid research operation or flags. Run siteos seo --help."
10603
10613
  );
10614
+ if (action.startsWith("serp ") && (!values.keyword?.trim() || values.keyword.length > 200))
10615
+ throw new Error(
10616
+ "Provide --keyword with 1 to 200 characters from the selected keyword report."
10617
+ );
10604
10618
  if (values.kind === "domain") values.kind = "competitors";
10605
10619
  if (operation.flags.includes("kind") && !kinds.includes(values.kind))
10606
10620
  throw new Error("Choose a documented --kind.");
@@ -10613,7 +10627,7 @@ async function runSeoResearchCommand(options) {
10613
10627
  const timeout = Number(values.timeout ?? 120);
10614
10628
  if (!Number.isInteger(timeout) || timeout < 1 || timeout > 900)
10615
10629
  throw new Error("Use a wait timeout between 1 and 900 seconds.");
10616
- if (action === "export" && (!values.output || !["json", "csv"].includes(values.format ?? "")))
10630
+ if (["export", "serp export"].includes(action) && (!values.output || !["json", "csv"].includes(values.format ?? "")))
10617
10631
  throw new Error(
10618
10632
  "Export requires --format json|csv and --output for a new file."
10619
10633
  );
@@ -10665,6 +10679,109 @@ async function runSeoResearchCommand(options) {
10665
10679
  );
10666
10680
  const query = `?kind=${encodeURIComponent(values.kind ?? "rankings")}`;
10667
10681
  let record;
10682
+ if (action.startsWith("serp ")) {
10683
+ const snapshotSchema = z19.object({
10684
+ id: z19.string(),
10685
+ organizationId: z19.literal(client.organizationId),
10686
+ resourceId: z19.literal(client.resourceId),
10687
+ sourceRunId: z19.literal(id),
10688
+ request: requestSchema,
10689
+ keywordKey: z19.literal(
10690
+ values.keyword.normalize("NFKC").trim().toLowerCase().replace(/\s+/gu, " ")
10691
+ ),
10692
+ state: z19.enum([
10693
+ "queued",
10694
+ "running",
10695
+ "completed",
10696
+ "failed",
10697
+ "cancelled"
10698
+ ]),
10699
+ dataset: z19.object({
10700
+ type: z19.literal("serp"),
10701
+ keyword: z19.string(),
10702
+ country: z19.string(),
10703
+ language: z19.string(),
10704
+ device: z19.enum(["desktop", "mobile"]),
10705
+ observedAt: z19.string(),
10706
+ rows: z19.array(
10707
+ z19.object({
10708
+ position: z19.number(),
10709
+ title: z19.string(),
10710
+ url: z19.string(),
10711
+ domain: z19.string(),
10712
+ description: z19.string()
10713
+ })
10714
+ )
10715
+ }).nullable()
10716
+ }).passthrough();
10717
+ const suffix = `/runs/${encodeURIComponent(id)}/serp`;
10718
+ const read = () => get(`${suffix}?keyword=${encodeURIComponent(values.keyword)}`);
10719
+ record = action === "serp ensure" ? await get(suffix, "run", { keyword: values.keyword }) : await read();
10720
+ const deadline = Date.now() + timeout * 1e3;
10721
+ let snapshot = record.serp == null ? null : snapshotSchema.parse(record.serp);
10722
+ if (action === "serp ensure" && !snapshot)
10723
+ throw new Error(
10724
+ "The server did not return the admitted SERP snapshot."
10725
+ );
10726
+ while (action === "serp wait" && snapshot && ["queued", "running"].includes(snapshot.state)) {
10727
+ if (Date.now() >= deadline)
10728
+ return {
10729
+ exitCode: 3,
10730
+ stdout: JSON.stringify({ ...record, timedOut: true }, null, 2)
10731
+ };
10732
+ await setTimeout2(Math.min(2e3, Math.max(0, deadline - Date.now())));
10733
+ record = await read();
10734
+ snapshot = record.serp == null ? null : snapshotSchema.parse(record.serp);
10735
+ }
10736
+ if (action === "serp export") {
10737
+ if (!snapshot)
10738
+ throw new Error("No saved SERP exists for this report and keyword.");
10739
+ const data = snapshot.dataset;
10740
+ const content = values.format === "json" ? JSON.stringify(record, null, 2) : [
10741
+ [
10742
+ "report_id",
10743
+ "keyword",
10744
+ "country",
10745
+ "language",
10746
+ "device",
10747
+ "state",
10748
+ "observed_at",
10749
+ "error",
10750
+ "position",
10751
+ "title",
10752
+ "url",
10753
+ "description"
10754
+ ],
10755
+ ...(data?.rows.length ? data.rows : [null]).map((row) => [
10756
+ id,
10757
+ values.keyword,
10758
+ data?.country,
10759
+ data?.language,
10760
+ data?.device,
10761
+ snapshot.state,
10762
+ data?.observedAt,
10763
+ snapshot.error,
10764
+ row?.position,
10765
+ row?.title,
10766
+ row?.url,
10767
+ row?.description
10768
+ ])
10769
+ ].map((row) => row.map(csvCell).join(",")).join("\r\n");
10770
+ const output = await writeSeoReport(options, values.output, content);
10771
+ record = {
10772
+ contractVersion: 1,
10773
+ runId: id,
10774
+ keyword: values.keyword,
10775
+ state: snapshot.state,
10776
+ format: values.format,
10777
+ output
10778
+ };
10779
+ }
10780
+ return {
10781
+ exitCode: action === "serp wait" && (!snapshot || ["failed", "cancelled"].includes(snapshot.state)) ? 4 : 0,
10782
+ stdout: JSON.stringify(record, null, 2)
10783
+ };
10784
+ }
10668
10785
  if (action === "plan" || action === "run" || action === "saved save") {
10669
10786
  const plan = await get("/plan", "read", input);
10670
10787
  resourceSchema.parse(plan.resource);