@gscdump/cli 7.11.1 → 7.12.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.
@@ -0,0 +1,38 @@
1
+ import { agentOutcomeReceiptSchema } from "@gscdump/contracts";
2
+ async function submitAgentOutcome(body, deps) {
3
+ const response = await deps.fetch(deps.url, {
4
+ method: "POST",
5
+ redirect: "error",
6
+ headers: {
7
+ "content-type": "application/json",
8
+ ...deps.credential ? { authorization: `Bearer ${deps.credential}` } : {}
9
+ },
10
+ body: JSON.stringify(body),
11
+ signal: AbortSignal.timeout(1e4)
12
+ }).catch((cause) => ({
13
+ _tag: "FetchFailed",
14
+ cause
15
+ }));
16
+ if ("_tag" in response) return {
17
+ _tag: "Err",
18
+ reason: "unreachable",
19
+ message: "Could not reach the outcome endpoint. Do not retry an uncertain submission."
20
+ };
21
+ if (!response.ok) return {
22
+ _tag: "Err",
23
+ reason: "rejected",
24
+ message: `Outcome reporting returned HTTP ${response.status}.`
25
+ };
26
+ const data = await response.json().catch((cause) => ({ invalidResponse: cause instanceof Error ? cause.message : String(cause) }));
27
+ const parsed = agentOutcomeReceiptSchema.safeParse(data);
28
+ if (!parsed.success) return {
29
+ _tag: "Err",
30
+ reason: "rejected",
31
+ message: "The outcome endpoint returned an invalid receipt."
32
+ };
33
+ return {
34
+ _tag: "Ok",
35
+ receipt: parsed.data
36
+ };
37
+ }
38
+ export { submitAgentOutcome };
@@ -1,14 +1,17 @@
1
+ import { useCliRuntime } from "./runtime.mjs";
1
2
  import { stopError } from "./stop.mjs";
2
3
  import { getHostedAccount, hostedCredential, siteManagerOf } from "./auth-state.mjs";
3
4
  import { logger } from "./utils.mjs";
4
5
  import { writeBingDump } from "./bing-data.mjs";
6
+ import { hostedRequestContext } from "./hosted-context.mjs";
5
7
  import { noSitesStop } from "./hosted-site.mjs";
6
8
  import { createGscdumpV1Client } from "@gscdump/sdk/v1";
7
9
  const BING_SITES_COMMAND = "gscdump bing sites --json";
8
10
  function hostedBingClient(state) {
9
11
  return createGscdumpV1Client({
10
12
  apiRoot: state.apiRoot,
11
- credential: hostedCredential(state)
13
+ credential: hostedCredential(state),
14
+ requestContext: hostedRequestContext(useCliRuntime().environment)
12
15
  });
13
16
  }
14
17
  async function listHostedBingSites(state) {
package/dist/cli.mjs CHANGED
@@ -7,7 +7,9 @@ import { resolveCliEnvironment } from "./environment.mjs";
7
7
  import { applyProfileFromCli } from "./commands/profile-selection.mjs";
8
8
  import { reportCliError } from "./error-handler.mjs";
9
9
  import { resolveOutputOptions, terminalOutputOptions } from "./render/terminal.mjs";
10
+ import { SKILL_AGENTS, inspectSkillInstallation } from "./skill.mjs";
10
11
  import { VERSION, setNoColor, showSplash, withConfiguredOutput } from "./utils.mjs";
12
+ import os from "node:os";
11
13
  import process from "node:process";
12
14
  import { defineCommand, renderUsage, runCommand } from "citty";
13
15
  function shouldShowSplash(rawArgs) {
@@ -139,6 +141,16 @@ async function runCli(opts = {}) {
139
141
  runtime.jsonOutput = asksForJson(rawArgs);
140
142
  const argumentError = await checkCliArgs(main, rawArgs);
141
143
  if (argumentError) throw commandLineError(argumentError);
144
+ if (rawArgs[0] !== "skill" && !rawArgs.some((arg) => HELP_FLAGS.has(arg) || arg === "--version") && runtime.environment.GSCDUMP_NO_SKILL_CHECK !== "1") {
145
+ const states = await Promise.all(SKILL_AGENTS.map((agent) => inspectSkillInstallation({
146
+ agent,
147
+ homeDirectory: os.homedir()
148
+ })));
149
+ for (const state of states) {
150
+ if (state.state === "stale") runtime.logger.warn(`The ${state.agent} skill differs from this CLI. Run gscdump skill install --agent ${state.agent}, then re-read SKILL.md.`);
151
+ if (state.state === "unavailable") runtime.logger.warn(`Could not check the ${state.agent} skill: ${state.message}`);
152
+ }
153
+ }
142
154
  await withConfiguredOutput(() => runMainCommand(rawArgs));
143
155
  return 0;
144
156
  } catch (error) {
@@ -88,8 +88,12 @@ const papercutCommandMeta = {
88
88
  name: "papercut",
89
89
  description: "Report a CLI problem to gscdump.com (anonymous, sanitized)"
90
90
  };
91
+ const telemetryCommandMeta = {
92
+ name: "telemetry",
93
+ description: "Record an explicit agent task outcome"
94
+ };
91
95
  const bingCommandMeta = {
92
96
  name: "bing",
93
97
  description: "Authenticate Bing, list sites, dump data, and read Indexing Evidence"
94
98
  };
95
- export { analyzeCommandMeta, authCommandMeta, bingCommandMeta, configCommandMeta, doctorCommandMeta, dumpCommandMeta, entitiesCommandMeta, indexingCommandMeta, initCommandMeta, inspectCommandMeta, mcpCommandMeta, papercutCommandMeta, profileCommandMeta, queryCommandMeta, reportCommandMeta, sitemapsCommandMeta, sitesCommandMeta, skillCommandMeta, storeCommandMeta, syncCommandMeta };
99
+ export { analyzeCommandMeta, authCommandMeta, bingCommandMeta, configCommandMeta, doctorCommandMeta, dumpCommandMeta, entitiesCommandMeta, indexingCommandMeta, initCommandMeta, inspectCommandMeta, mcpCommandMeta, papercutCommandMeta, profileCommandMeta, queryCommandMeta, reportCommandMeta, sitemapsCommandMeta, sitesCommandMeta, skillCommandMeta, storeCommandMeta, syncCommandMeta, telemetryCommandMeta };
@@ -1,4 +1,4 @@
1
- import { analyzeCommandMeta, authCommandMeta, bingCommandMeta, configCommandMeta, doctorCommandMeta, dumpCommandMeta, entitiesCommandMeta, indexingCommandMeta, initCommandMeta, inspectCommandMeta, mcpCommandMeta, papercutCommandMeta, profileCommandMeta, queryCommandMeta, reportCommandMeta, sitemapsCommandMeta, sitesCommandMeta, skillCommandMeta, storeCommandMeta, syncCommandMeta } from "./command-meta.mjs";
1
+ import { analyzeCommandMeta, authCommandMeta, bingCommandMeta, configCommandMeta, doctorCommandMeta, dumpCommandMeta, entitiesCommandMeta, indexingCommandMeta, initCommandMeta, inspectCommandMeta, mcpCommandMeta, papercutCommandMeta, profileCommandMeta, queryCommandMeta, reportCommandMeta, sitemapsCommandMeta, sitesCommandMeta, skillCommandMeta, storeCommandMeta, syncCommandMeta, telemetryCommandMeta } from "./command-meta.mjs";
2
2
  function resolveValue(value) {
3
3
  return typeof value === "function" ? value() : value;
4
4
  }
@@ -130,6 +130,7 @@ const CLI_SUBCOMMANDS = {
130
130
  doctor: shallowCommand(doctorCommandMeta, () => import("./commands/doctor.mjs").then((module) => module.doctorCommand)),
131
131
  mcp: shallowCommand(mcpCommandMeta, () => import("./commands/mcp.mjs").then((module) => module.mcpCommand)),
132
132
  skill: shallowCommand(skillCommandMeta, () => import("./commands/skill.mjs").then((module) => module.skillCommand)),
133
- papercut: shallowCommand(papercutCommandMeta, () => import("./commands/papercut.mjs").then((module) => module.papercutCommand))
133
+ papercut: shallowCommand(papercutCommandMeta, () => import("./commands/papercut.mjs").then((module) => module.papercutCommand)),
134
+ telemetry: shallowCommand(telemetryCommandMeta, () => import("./commands/telemetry.mjs").then((module) => module.telemetryCommand))
134
135
  };
135
136
  export { CLI_SUBCOMMANDS, findSubCommandIndex, isUsageError, resolveUsageTarget, usageError };
@@ -34,6 +34,7 @@ function loginNextStep(state, siteCount, issuer) {
34
34
  const HOSTED_COMMANDS = [
35
35
  "sites",
36
36
  "query",
37
+ "report",
37
38
  "sitemaps current",
38
39
  "sitemaps history",
39
40
  "sitemaps membership",
@@ -8,24 +8,60 @@ import { confirm, isCancel } from "@clack/prompts";
8
8
  const papercutCommand = defineCommand({
9
9
  meta: papercutCommandMeta,
10
10
  args: {
11
- command: {
11
+ "command": {
12
12
  type: "string",
13
13
  description: "The gscdump command that misbehaved, for example \"report triage\""
14
14
  },
15
- comment: {
15
+ "comment": {
16
16
  type: "string",
17
17
  description: "What you ran, what you expected, what happened, any workaround (sanitized)"
18
18
  },
19
- agent: {
19
+ "agent": {
20
20
  type: "string",
21
21
  description: "Reporter name, for example \"Claude Code\" or a person's handle"
22
22
  },
23
- intent: {
23
+ "intent": {
24
24
  type: "string",
25
25
  description: "bug (default) or improvement",
26
26
  default: "bug"
27
27
  },
28
- yes: {
28
+ "request-id": {
29
+ type: "string",
30
+ description: "Request ID returned by the failed operation"
31
+ },
32
+ "parent-request-id": {
33
+ type: "string",
34
+ description: "Request ID returned by the calling application"
35
+ },
36
+ "workflow-id": {
37
+ type: "string",
38
+ description: "Opaque task identifier without customer content"
39
+ },
40
+ "origin": {
41
+ type: "string",
42
+ description: "Application host, for example requestindexing.com"
43
+ },
44
+ "product": {
45
+ type: "string",
46
+ description: "Application identifier"
47
+ },
48
+ "surface": {
49
+ type: "string",
50
+ description: "cli, mcp, api, skill, or browser"
51
+ },
52
+ "operation": {
53
+ type: "string",
54
+ description: "Operation identifier"
55
+ },
56
+ "skill-digest": {
57
+ type: "string",
58
+ description: "SHA-256 digest of the skill artifact"
59
+ },
60
+ "skill-version": {
61
+ type: "string",
62
+ description: "Declared skill version"
63
+ },
64
+ "yes": {
29
65
  type: "boolean",
30
66
  alias: "y",
31
67
  default: false,
@@ -39,7 +75,16 @@ const papercutCommand = defineCommand({
39
75
  command: args.command,
40
76
  comment: args.comment,
41
77
  agent: args.agent,
42
- intent: args.intent
78
+ intent: args.intent,
79
+ requestId: args["request-id"],
80
+ parentRequestId: args["parent-request-id"],
81
+ workflowId: args["workflow-id"],
82
+ origin: args.origin,
83
+ product: args.product,
84
+ surface: args.surface,
85
+ operation: args.operation,
86
+ skillDigest: args["skill-digest"],
87
+ skillVersion: args["skill-version"]
43
88
  });
44
89
  if (built._tag === "Err") {
45
90
  logger.error(`Invalid papercut: ${built.message}`);
@@ -1,8 +1,8 @@
1
1
  import { useCliRuntime } from "../runtime.mjs";
2
2
  import { setConfigDir } from "../config.mjs";
3
3
  import { resolveCliEnvironment } from "../environment.mjs";
4
- import path from "node:path";
5
4
  import os from "node:os";
5
+ import path from "node:path";
6
6
  import fs from "node:fs";
7
7
  const ROOT_DIR = path.join(os.homedir(), ".config", "gscdump");
8
8
  const PROFILES_DIR = path.join(ROOT_DIR, "profiles");
@@ -1,8 +1,10 @@
1
+ import { resolveAuthentication } from "../auth-state.mjs";
1
2
  import { reportCommandMeta } from "../command-meta.mjs";
2
3
  import { terminalOutputOptions } from "../render/terminal.mjs";
3
4
  import { logger, parseFetchBudget, setJsonOutput } from "../utils.mjs";
4
5
  import { COMPARISON_FLAGS, PERIOD_FLAGS, parseWindowFlags, windowFlagErrorToException } from "../window.mjs";
5
6
  import { analysisNeeds, analyzerSources, analyzerTables, resolveAnalysisSource } from "../analysis-local.mjs";
7
+ import { runHostedReport } from "../hosted-report.mjs";
6
8
  import { renderCliReport } from "../render/report.mjs";
7
9
  import { defineCommand } from "citty";
8
10
  import { getLatestGscDate } from "gscdump/dates";
@@ -109,10 +111,15 @@ function makeReportCommand(report) {
109
111
  default: false,
110
112
  description: "Force live GSC API; bypass local store"
111
113
  },
114
+ "force": {
115
+ type: "boolean",
116
+ default: false,
117
+ description: "Regenerate a Hosted report instead of reading its cache"
118
+ },
112
119
  "json": {
113
120
  type: "boolean",
114
121
  default: false,
115
- description: "Emit full ReportResult JSON"
122
+ description: "Emit report JSON with evidence coverage"
116
123
  },
117
124
  "explain": {
118
125
  type: "boolean",
@@ -147,6 +154,22 @@ function makeReportCommand(report) {
147
154
  }, null, 2));
148
155
  return;
149
156
  }
157
+ if (!args.live && (await resolveAuthentication())._tag === "Hosted") {
158
+ const result = await runHostedReport(report.id, args);
159
+ if (args.json) {
160
+ console.log(JSON.stringify(result, null, 2));
161
+ return;
162
+ }
163
+ console.log(`${report.id}: ${result.site}\n${result.window.start} to ${result.window.end}`);
164
+ for (const section of result.sections) {
165
+ console.log(`\n${section.title}${section.dataUnavailable ? " (unavailable)" : ""}`);
166
+ for (const finding of section.findings) console.log(`${finding.entity.value}: ${Object.entries(finding.metrics).map(([key, value]) => `${key}=${value}`).join(", ")}`);
167
+ if (section.truncated) logger.warn(`${section.id}: ${section.truncated.kept} of ${section.truncated.total} findings returned.`);
168
+ }
169
+ if (result.meta.degraded) logger.warn("Report evidence is incomplete. Read section coverage before drawing conclusions.");
170
+ return;
171
+ }
172
+ if (args.force) throw new Error("--force regenerates a Hosted report. Select Hosted mode to use it.");
150
173
  const steps = (window) => report.plan(params, window).map((step) => ({
151
174
  type: step.type,
152
175
  params: {
@@ -1,9 +1,9 @@
1
1
  import { skillCommandMeta } from "../command-meta.mjs";
2
+ import { SKILL_AGENTS, SKILL_NAME, inspectSkillInstallation, installSkill } from "../skill.mjs";
2
3
  import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
3
- import { SKILL_AGENTS, SKILL_NAME, installSkill } from "../skill.mjs";
4
+ import os from "node:os";
4
5
  import process from "node:process";
5
6
  import { defineCommand } from "citty";
6
- import os from "node:os";
7
7
  const installCommand = defineCommand({
8
8
  meta: {
9
9
  name: "install",
@@ -44,6 +44,37 @@ const installCommand = defineCommand({
44
44
  });
45
45
  const skillCommand = defineCommand({
46
46
  meta: skillCommandMeta,
47
- subCommands: { install: installCommand }
47
+ subCommands: {
48
+ install: installCommand,
49
+ status: defineCommand({
50
+ meta: {
51
+ name: "status",
52
+ description: "Compare installed skill content with the packaged artifact"
53
+ },
54
+ args: {
55
+ agent: {
56
+ type: "string",
57
+ default: "claude",
58
+ description: `Agent skill directory: ${SKILL_AGENTS.join(" or ")}`
59
+ },
60
+ target: {
61
+ type: "string",
62
+ description: "Inspect this directory instead of the agent home directory"
63
+ },
64
+ ...OUTPUT_ARGS
65
+ },
66
+ async run({ args }) {
67
+ applyOutputMode(args);
68
+ const agent = SKILL_AGENTS.find((name) => name === args.agent);
69
+ if (!agent) throw new Error(`Unknown agent "${args.agent}". Use ${SKILL_AGENTS.join(" or ")}.`);
70
+ const result = await inspectSkillInstallation({
71
+ agent,
72
+ homeDirectory: os.homedir(),
73
+ target: args.target
74
+ });
75
+ console.log(JSON.stringify(result));
76
+ }
77
+ })
78
+ }
48
79
  });
49
80
  export { skillCommand };
@@ -0,0 +1,142 @@
1
+ import { useCliRuntime } from "../runtime.mjs";
2
+ import { hostedCredential, resolveAuthentication } from "../auth-state.mjs";
3
+ import { OUTPUT_ARGS, VERSION, applyOutputMode } from "../utils.mjs";
4
+ import { resolvePapercutUrl } from "../papercut.mjs";
5
+ import { submitAgentOutcome } from "../agent-outcome.mjs";
6
+ import process from "node:process";
7
+ import { defineCommand } from "citty";
8
+ import { randomUUID } from "node:crypto";
9
+ import { agentOutcomeBodySchema } from "@gscdump/contracts";
10
+ import { confirm, isCancel } from "@clack/prompts";
11
+ const telemetryCommand = defineCommand({
12
+ meta: {
13
+ name: "telemetry",
14
+ description: "Record an explicit agent task outcome"
15
+ },
16
+ subCommands: { record: defineCommand({
17
+ meta: {
18
+ name: "record",
19
+ description: "Submit bounded task metadata without prompts or output bodies"
20
+ },
21
+ args: {
22
+ "event-id": {
23
+ type: "string",
24
+ description: "Stable event ID; defaults to a new UUID"
25
+ },
26
+ "workflow-id": {
27
+ type: "string",
28
+ required: true,
29
+ description: "Opaque task identifier"
30
+ },
31
+ "request-id": {
32
+ type: "string",
33
+ description: "Related server request ID"
34
+ },
35
+ "parent-request-id": {
36
+ type: "string",
37
+ description: "Related consumer request ID"
38
+ },
39
+ "product": {
40
+ type: "string",
41
+ default: "gscdump",
42
+ description: "Declared originating application"
43
+ },
44
+ "surface": {
45
+ type: "string",
46
+ default: "cli",
47
+ description: "cli, mcp, api, skill, or browser"
48
+ },
49
+ "operation": {
50
+ type: "string",
51
+ required: true,
52
+ description: "Operation identifier without arguments"
53
+ },
54
+ "step": {
55
+ type: "string",
56
+ default: "complete",
57
+ description: "setup, read, diagnose, fix, verify, or complete"
58
+ },
59
+ "outcome": {
60
+ type: "string",
61
+ required: true,
62
+ description: "success, failure, blocked, or cancelled"
63
+ },
64
+ "semantic-flags": {
65
+ type: "string",
66
+ description: "Comma-separated empty, unavailable, refused, provisional, partial, stale, truncated"
67
+ },
68
+ "row-count": {
69
+ type: "string",
70
+ description: "Measured domain row count"
71
+ },
72
+ "duration-ms": {
73
+ type: "string",
74
+ description: "Measured task duration"
75
+ },
76
+ "quality": {
77
+ type: "string",
78
+ default: "unknown",
79
+ description: "Declared useful, unusable, incorrect, or unknown"
80
+ },
81
+ "error-code": {
82
+ type: "string",
83
+ description: "Stable error code"
84
+ },
85
+ "skill-digest": {
86
+ type: "string",
87
+ description: "SHA-256 digest of the skill artifact"
88
+ },
89
+ "skill-version": {
90
+ type: "string",
91
+ description: "Declared skill version"
92
+ },
93
+ "yes": {
94
+ type: "boolean",
95
+ alias: "y",
96
+ default: false,
97
+ description: "Submit without confirmation"
98
+ },
99
+ ...OUTPUT_ARGS
100
+ },
101
+ async run({ args }) {
102
+ const { json } = applyOutputMode(args);
103
+ const body = agentOutcomeBodySchema.parse({
104
+ eventId: args["event-id"] || randomUUID(),
105
+ workflowId: args["workflow-id"],
106
+ requestId: args["request-id"],
107
+ parentRequestId: args["parent-request-id"],
108
+ product: args.product,
109
+ surface: args.surface,
110
+ operation: args.operation,
111
+ step: args.step,
112
+ outcome: args.outcome,
113
+ semanticFlags: args["semantic-flags"] ? args["semantic-flags"].split(",").map((flag) => flag.trim()) : [],
114
+ rowCount: args["row-count"] !== void 0 ? Number(args["row-count"]) : void 0,
115
+ durationMs: args["duration-ms"] !== void 0 ? Number(args["duration-ms"]) : void 0,
116
+ quality: args.quality,
117
+ errorCode: args["error-code"],
118
+ skillDigest: args["skill-digest"],
119
+ skillVersion: args["skill-version"],
120
+ cliVersion: VERSION
121
+ });
122
+ if (!args.yes) {
123
+ if (!process.stdin.isTTY || json) throw new Error("Pass --yes to submit this declared task outcome.");
124
+ const accepted = await confirm({ message: "Submit this declared agent task outcome?" });
125
+ if (isCancel(accepted) || !accepted) return;
126
+ }
127
+ const runtime = useCliRuntime();
128
+ const url = resolvePapercutUrl(runtime.environment).replace(/\/papercuts$/, "/agent-outcomes");
129
+ const authentication = await resolveAuthentication();
130
+ if (authentication._tag === "Hosted" && new URL(url).origin !== new URL(authentication.apiRoot).origin) throw new Error("The outcome endpoint does not match the Hosted credential origin.");
131
+ const credential = authentication._tag === "Hosted" ? hostedCredential(authentication) : void 0;
132
+ const result = await submitAgentOutcome(body, {
133
+ fetch: globalThis.fetch,
134
+ url,
135
+ credential
136
+ });
137
+ if (result._tag === "Err") throw new Error(result.message);
138
+ console.log(JSON.stringify(result.receipt));
139
+ }
140
+ }) }
141
+ });
142
+ export { telemetryCommand };
package/dist/config.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { useCliRuntime } from "./runtime.mjs";
2
+ import os from "node:os";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { z } from "zod";
5
- import os from "node:os";
6
6
  import { SearchTypes } from "gscdump/query";
7
7
  function setConfigDir(dir) {
8
8
  useCliRuntime().configDir = dir;
@@ -1,8 +1,8 @@
1
1
  import { exportColumns, readParquetSql, sourceSelectSql, sqlString } from "./table-sources.mjs";
2
+ import os from "node:os";
2
3
  import { createHash } from "node:crypto";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "node:path";
5
- import os from "node:os";
6
6
  import { DuckDBInstance } from "@duckdb/node-api";
7
7
  import { encodeRowsToParquetFlex } from "@gscdump/engine/hyparquet";
8
8
  const DUMP_FORMATS = [
@@ -0,0 +1,13 @@
1
+ import { VERSION } from "./utils.mjs";
2
+ import { agentRequestContextSchema } from "@gscdump/contracts";
3
+ function hostedRequestContext(environment) {
4
+ return agentRequestContextSchema.parse({
5
+ cliVersion: VERSION,
6
+ client: "gscdump-cli",
7
+ product: environment.GSCDUMP_PRODUCT ?? "gscdump",
8
+ workflowId: environment.GSCDUMP_WORKFLOW_ID,
9
+ parentRequestId: environment.GSCDUMP_PARENT_REQUEST_ID,
10
+ skillDigest: environment.GSCDUMP_SKILL_DIGEST
11
+ });
12
+ }
13
+ export { hostedRequestContext };
@@ -0,0 +1,40 @@
1
+ import { resolveHostedSite } from "./hosted-site.mjs";
2
+ import { runReportV1RequestSchema } from "@gscdump/contracts/v1";
3
+ function hostedReportRequest(reportId, args) {
4
+ if (args["prev-start"] || args["prev-end"] || args["fetch-budget"]) throw new Error("Hosted reports do not accept --prev-start, --prev-end, or --fetch-budget. Use --vs for comparison.");
5
+ const period = args.period ? String(args.period).replace(/^(\d+d)$/, "last-$1") : void 0;
6
+ return runReportV1RequestSchema.parse({
7
+ reportId,
8
+ period,
9
+ comparison: args.vs,
10
+ start: args.start,
11
+ end: args.end,
12
+ target: args.target,
13
+ targetKind: args["target-kind"],
14
+ brandTerms: args["brand-terms"],
15
+ topic: args.topic,
16
+ maxFindings: args["max-findings"] !== void 0 ? Number(args["max-findings"]) : void 0,
17
+ minClicksChange: args["min-clicks-change"] !== void 0 ? Number(args["min-clicks-change"]) : void 0,
18
+ force: Boolean(args.force)
19
+ });
20
+ }
21
+ async function runHostedReport(reportId, args) {
22
+ const body = hostedReportRequest(reportId, args);
23
+ const { client, site } = await resolveHostedSite(args, {
24
+ name: `report ${reportId}`,
25
+ localAlternative: "run the report against your Store"
26
+ });
27
+ const response = await client.runReport({
28
+ params: { siteId: site.siteId },
29
+ body
30
+ });
31
+ return {
32
+ ...response.data,
33
+ meta: {
34
+ ...response.data.meta,
35
+ requestId: response.meta.requestId,
36
+ source: "hosted"
37
+ }
38
+ };
39
+ }
40
+ export { hostedReportRequest, runHostedReport };
@@ -2,6 +2,7 @@ import { stopError } from "./stop.mjs";
2
2
  import { connectSiteStep, getHostedAccount, hostedCredential, parseAuthentication, resolveAuthentication, siteManagerOf } from "./auth-state.mjs";
3
3
  import { loadConfig } from "./config.mjs";
4
4
  import { resolveCliEnvironment } from "./environment.mjs";
5
+ import { hostedRequestContext } from "./hosted-context.mjs";
5
6
  import { createGscdumpV1Client } from "@gscdump/sdk/v1";
6
7
  const HOSTED_ARGS = {
7
8
  "site": {
@@ -104,7 +105,8 @@ async function resolveHostedSite(args, command) {
104
105
  return {
105
106
  client: createGscdumpV1Client({
106
107
  apiRoot: authentication.apiRoot,
107
- credential: hostedCredential(authentication)
108
+ credential: hostedCredential(authentication),
109
+ requestContext: hostedRequestContext(environment)
108
110
  }),
109
111
  site: match.site,
110
112
  authentication,
package/dist/package.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var version = "7.11.1";
1
+ var version = "7.12.1";
2
2
  export { version };
package/dist/papercut.mjs CHANGED
@@ -1,28 +1,18 @@
1
1
  import { VERSION } from "./utils.mjs";
2
2
  import process from "node:process";
3
3
  import { z } from "zod";
4
+ import { cliPapercutBodySchema } from "@gscdump/contracts";
4
5
  const DEFAULT_PAPERCUT_BASE_URL = "https://gscdump.com";
5
6
  const PAPERCUT_PATH = "/api/cli/papercuts";
6
7
  const REQUEST_TIMEOUT_MS = 1e4;
7
- const singleLine = (max) => z.string().trim().min(1).max(max).regex(/^\P{Cc}*$/u, "must be a single line without control characters");
8
- const papercutBodySchema = z.strictObject({
9
- command: singleLine(200),
10
- comment: z.string().trim().min(1).max(2e3),
11
- agent: singleLine(100),
12
- intent: z.enum(["bug", "improvement"]).default("bug"),
13
- cliVersion: singleLine(64),
14
- node: singleLine(64),
15
- platform: singleLine(32)
16
- });
8
+ const papercutBodySchema = cliPapercutBodySchema;
17
9
  const receiptSchema = z.object({
18
10
  id: z.string().min(1),
19
11
  status: z.literal("new")
20
12
  });
21
13
  function buildPapercutBody(input) {
22
14
  const parsed = papercutBodySchema.safeParse({
23
- command: input.command,
24
- comment: input.comment,
25
- agent: input.agent,
15
+ ...input,
26
16
  intent: input.intent ?? "bug",
27
17
  cliVersion: VERSION,
28
18
  node: process.versions.node,
@@ -77,14 +67,14 @@ async function submitPapercut(body, deps) {
77
67
  if (response.status === 400 || response.status === 422) return {
78
68
  _tag: "Err",
79
69
  reason: "invalid_input",
80
- message: `gscdump.com rejected the papercut: ${(await response.text().catch(() => "")).slice(0, 300) || response.status}`
70
+ message: `gscdump.com rejected the papercut: ${(await response.text()).slice(0, 300) || response.status}`
81
71
  };
82
72
  if (!response.ok) return {
83
73
  _tag: "Err",
84
74
  reason: "rejected",
85
75
  message: `gscdump.com answered ${response.status}.`
86
76
  };
87
- const json = await response.json().catch(() => null);
77
+ const json = await response.json().catch((cause) => ({ invalidResponse: cause instanceof Error ? cause.message : String(cause) }));
88
78
  const receipt = receiptSchema.safeParse(json);
89
79
  if (!receipt.success) return {
90
80
  _tag: "Err",
package/dist/runtime.mjs CHANGED
@@ -1,7 +1,7 @@
1
+ import os from "node:os";
1
2
  import process from "node:process";
2
3
  import path from "node:path";
3
4
  import { AsyncLocalStorage } from "node:async_hooks";
4
- import os from "node:os";
5
5
  import { createConsola } from "consola";
6
6
  let runtimeStorage;
7
7
  let fallbackRuntime;
package/dist/skill.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { cp, mkdir, stat } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { cp, mkdir, mkdtemp, readFile, readdir, rename, rm, stat } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  const SKILL_NAME = "gscdump";
@@ -13,6 +14,78 @@ function skillSourceDirectory(moduleUrl = import.meta.url) {
13
14
  function skillDestination(homeDirectory, agent) {
14
15
  return path.join(homeDirectory, AGENT_DIRECTORIES[agent], "skills", SKILL_NAME);
15
16
  }
17
+ async function skillDigest(directory) {
18
+ const hash = createHash("sha256");
19
+ async function read(relative) {
20
+ const entries = await readdir(path.join(directory, relative), { withFileTypes: true });
21
+ entries.sort((a, b) => a.name.localeCompare(b.name));
22
+ for (const entry of entries) {
23
+ if (entry.name.startsWith(".")) continue;
24
+ const name = relative ? `${relative}/${entry.name}` : entry.name;
25
+ if (entry.isDirectory()) await read(name);
26
+ else if (entry.isFile()) hash.update(name).update("\0").update(await readFile(path.join(directory, name))).update("\0");
27
+ else throw new Error(`The skill contains an unsupported file at ${name}.`);
28
+ }
29
+ }
30
+ await read("");
31
+ return hash.digest("hex");
32
+ }
33
+ async function inspectSkillInstallation(options) {
34
+ const destination = options.target ? path.join(options.target, SKILL_NAME) : skillDestination(options.homeDirectory, options.agent);
35
+ if (!await stat(destination).then(() => true).catch((cause) => {
36
+ if (cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT") return false;
37
+ throw cause;
38
+ })) return {
39
+ state: "not-installed",
40
+ agent: options.agent,
41
+ destination
42
+ };
43
+ return Promise.all([skillDigest(destination), skillDigest(options.sourceDirectory ?? skillSourceDirectory())]).then(([installedDigest, packagedDigest]) => ({
44
+ state: installedDigest === packagedDigest ? "current" : "stale",
45
+ agent: options.agent,
46
+ destination,
47
+ installedDigest,
48
+ packagedDigest
49
+ })).catch((cause) => ({
50
+ state: "unavailable",
51
+ agent: options.agent,
52
+ destination,
53
+ message: cause instanceof Error ? cause.message : String(cause)
54
+ }));
55
+ }
56
+ async function replaceSkill(source, destination) {
57
+ await mkdir(path.dirname(destination), { recursive: true });
58
+ const staging = await mkdtemp(path.join(path.dirname(destination), ".gscdump-skill-"));
59
+ const candidate = path.join(staging, "candidate");
60
+ const backup = path.join(staging, "previous");
61
+ const digest = await cp(source, candidate, {
62
+ recursive: true,
63
+ force: true
64
+ }).then(() => skillDigest(candidate)).catch(async (cause) => {
65
+ await rm(staging, {
66
+ recursive: true,
67
+ force: true
68
+ });
69
+ throw cause;
70
+ });
71
+ const previous = await rename(destination, backup).then(() => "moved").catch((cause) => {
72
+ if (cause !== null && typeof cause === "object" && "code" in cause && cause.code === "ENOENT") return "absent";
73
+ throw cause;
74
+ });
75
+ await rename(candidate, destination).catch(async (cause) => {
76
+ if (previous === "moved") await rename(backup, destination);
77
+ await rm(staging, {
78
+ recursive: true,
79
+ force: true
80
+ });
81
+ throw cause;
82
+ });
83
+ await rm(staging, {
84
+ recursive: true,
85
+ force: true
86
+ });
87
+ return digest;
88
+ }
16
89
  async function installSkill(options) {
17
90
  const source = options.sourceDirectory ?? skillSourceDirectory();
18
91
  if (!(await stat(source).catch(() => {
@@ -23,15 +96,13 @@ async function installSkill(options) {
23
96
  message: `The packaged skill is missing at ${source}. Reinstall @gscdump/cli, then run the command again.`
24
97
  };
25
98
  const destination = options.target ? path.join(options.target, SKILL_NAME) : skillDestination(options.homeDirectory, options.agent);
26
- return mkdir(path.dirname(destination), { recursive: true }).then(() => cp(source, destination, {
27
- recursive: true,
28
- force: true
29
- })).then(() => ({
99
+ return replaceSkill(source, destination).then((digest) => ({
30
100
  _tag: "Ok",
31
101
  installation: {
32
102
  agent: options.agent,
33
103
  source,
34
- destination
104
+ destination,
105
+ digest
35
106
  }
36
107
  })).catch((cause) => {
37
108
  const detail = cause instanceof Error ? cause.message : String(cause);
@@ -42,4 +113,4 @@ async function installSkill(options) {
42
113
  };
43
114
  });
44
115
  }
45
- export { SKILL_AGENTS, SKILL_NAME, installSkill, skillDestination, skillSourceDirectory };
116
+ export { SKILL_AGENTS, SKILL_NAME, inspectSkillInstallation, installSkill, skillDestination, skillSourceDirectory };
package/dist/utils.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { useCliRuntime } from "./runtime.mjs";
2
2
  import { commandLineError } from "./stop.mjs";
3
3
  import { version } from "./package.mjs";
4
+ import os from "node:os";
4
5
  import process from "node:process";
5
6
  import fs from "node:fs/promises";
6
- import os from "node:os";
7
7
  import { SearchTypes } from "gscdump/query";
8
8
  import { Buffer } from "node:buffer";
9
9
  import { MAX_FETCH_BUDGET } from "@gscdump/engine/analysis-types";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "7.11.1",
4
+ "version": "7.12.1",
5
5
  "description": "CLI for Google Search Console and Bing with hosted or local authentication, data exports, and an MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -44,16 +44,16 @@
44
44
  "dependencies": {
45
45
  "@clack/prompts": "^1.8.1",
46
46
  "@duckdb/node-api": "1.5.5-r.5",
47
- "@gscdump/analysis": "^7.11.1",
48
- "@gscdump/contracts": "^7.11.1",
49
- "@gscdump/engine": "^7.11.1",
50
- "@gscdump/engine-gsc-api": "^7.11.1",
51
- "@gscdump/sdk": "^7.11.1",
47
+ "@gscdump/analysis": "^7.12.1",
48
+ "@gscdump/contracts": "^7.12.1",
49
+ "@gscdump/engine": "^7.12.1",
50
+ "@gscdump/engine-gsc-api": "^7.12.1",
51
+ "@gscdump/sdk": "^7.12.1",
52
52
  "@modelcontextprotocol/sdk": "^1.30.1",
53
53
  "citty": "^0.2.2",
54
54
  "consola": "^3.4.2",
55
55
  "google-auth-library": "^11.1.0",
56
- "gscdump": "^7.11.1",
56
+ "gscdump": "^7.12.1",
57
57
  "ofetch": "^1.5.1",
58
58
  "open": "^11.0.4",
59
59
  "proper-lockfile": "^4.1.2",
@@ -18,6 +18,10 @@ Example: `gscdump query --site=SITE --start=DATE --end=DATE -d page -f json`.
18
18
 
19
19
  ## Start each task
20
20
 
21
+ Run `gscdump skill status --agent codex --json` for Codex, or use `--agent claude` for Claude.
22
+ If the result is `stale`, reinstall that agent's skill and re-read it before continuing.
23
+ The check compares the complete artifact, including references. A matching version alone does not prove matching instructions.
24
+
21
25
  1. Before reading traffic, run `gscdump auth status --json`. Do this even when the user says authentication works.
22
26
  2. Keep the requested Site, dates, dimensions, and task scope. A request for pages does not need query dimensions.
23
27
  3. In Hosted mode, skip steps 3 to 5. `gscdump query` reads the hosted record, and `sync` needs Local mode.
@@ -73,7 +77,7 @@ gscdump sites --json
73
77
  gscdump query --site example.com -d page -f json
74
78
  ```
75
79
 
76
- Hosted mode can run only these commands: `sites`, `query`, `sitemaps current`, `sitemaps history`, `sitemaps membership`,
80
+ Hosted mode can run these commands: `sites`, `query`, `report`, `sitemaps current`, `sitemaps history`, `sitemaps membership`,
77
81
  `sitemaps lastmod`, `sitemaps export`, `sitemaps inspect`, `indexing urls`, `indexing inspect --yes`, `indexing summary`, `indexing watch list|add|remove`,
78
82
  and the `bing` commands `login --site`, `sites`, `status`, `dump`, `inspect`, and `verify`.
79
83
  Direct Google commands need Local mode: `sync`, `inspect`, `query --live`, `analyze --live`, `report --live`,
@@ -119,7 +123,13 @@ Hosted sitemap reads and the hosted `indexing` commands need Hosted mode. Their
119
123
  ## Routing
120
124
 
121
125
  This section covers Local mode. In Hosted mode, `query` always reads the hosted record, and JSON has `meta.source: "hosted"`.
122
- `analyze` and `report` read the Store only in Hosted mode. When the Store cannot answer, they stop with the Local mode command.
126
+ `report` runs deterministic reports through the hosted record in Hosted mode. It needs no local Store.
127
+ Its JSON preserves `cached`, `generatedAt`, the resolved `window`, section `coverage`, `truncated`, and `dataUnavailable`.
128
+ Its `meta.requestId` correlates successful reads with feedback. `meta.source` is `hosted`.
129
+ A section with `dataUnavailable: true` is unavailable evidence, not an empty finding.
130
+ `--force` regenerates the Hosted report. `--prev-start`, `--prev-end`, and `--fetch-budget` need Local mode.
131
+ Hosted reports preserve `--max-findings` from 1 to 1000. Movers also preserves `--min-clicks-change`.
132
+ `analyze` reads the Store only in Hosted mode. When the Store cannot answer, it stops with the Local mode command.
123
133
 
124
134
  `query`, `analyze`, and `report` choose one source for each run. One run never
125
135
  mixes Store rows and live rows.
@@ -292,6 +302,7 @@ A routing stop also has `siteUrl`. `STORE_RANGE_NOT_COVERED` adds `missingDates`
292
302
  | `gscdump mcp` | Start Google MCP tools in Local mode. Hosted mode uses `https://gscdump.com/mcp` |
293
303
  | `gscdump skill install` | Copy this skill into an agent skill directory |
294
304
  | `gscdump papercut` | Report a CLI problem to gscdump.com |
305
+ | `gscdump telemetry record` | Explicitly record bounded, declared task quality without prompts or output bodies |
295
306
 
296
307
  `gscdump login`, `gscdump logout`, and `gscdump status` are top-level aliases of
297
308
  the matching `auth` subcommands.
@@ -404,6 +415,11 @@ gscdump papercut --command "report triage" --agent "Claude Code" \
404
415
  - Include what you ran, what you expected, what happened, and any workaround.
405
416
  - `--intent improvement` marks a suggestion. The default is `bug`.
406
417
  - The CLI adds its version, Node version, and platform.
418
+ - Add `--request-id` when the failed operation returned one.
419
+ - Add `--parent-request-id` when a consumer returned a separate request ID.
420
+ - Use `--origin requestindexing.com --product requestindexing` for work started there.
421
+ - `--surface cli|mcp|api|skill|browser`, `--operation`, `--workflow-id`, `--skill-digest`, and `--skill-version` add safe context.
422
+ - These fields are declared context. They do not prove authorization or credential origin.
407
423
  - Send sanitized details only. Remove tokens, cookies, personal data, private
408
424
  URLs, and customer rows. Use placeholder arguments.
409
425
 
@@ -415,6 +431,29 @@ The endpoint is anonymous and allows ten reports per network address each
415
431
  hour. Success returns `id` and `status: "new"`. If reporting fails, mention
416
432
  the failure and continue. Never retry an uncertain submission.
417
433
 
434
+ Hosted requests send the CLI version and client identity. Optional environment variables add declared context:
435
+ `GSCDUMP_PRODUCT`, `GSCDUMP_WORKFLOW_ID`, `GSCDUMP_PARENT_REQUEST_ID`, and `GSCDUMP_SKILL_DIGEST`.
436
+ Use opaque IDs, never prompts, private URLs, or customer rows. No local command history uploads automatically.
437
+
438
+ ## Record task quality
439
+
440
+ After an agent workflow, record one explicit terminal outcome when reporting is allowed.
441
+ Reuse one opaque workflow ID throughout the task. Never put customer content in an ID.
442
+
443
+ ```sh
444
+ gscdump telemetry record --workflow-id task_01 --operation indexing.summary \
445
+ --outcome success --quality useful --yes --json
446
+ ```
447
+
448
+ Use `failure`, `blocked`, or `cancelled` when the task did not complete.
449
+ Add measured `--row-count` and `--duration-ms` only when known.
450
+ Use `--semantic-flags` for simultaneous conditions, such as `partial,stale,truncated`.
451
+ Add request IDs, product, and skill digest when available. Use `--quality unknown` when usefulness is unmeasured.
452
+ This records an agent claim. It never proves that a repair passed engine verification.
453
+ Record incorrect or unusable answers, and submit a separate sanitized papercut for reproduction.
454
+ This skill authorizes bounded task-outcome reports, subject to the user's reporting restrictions.
455
+ If submission fails, mention it and continue. Never retry an uncertain submission.
456
+
418
457
  ## Guardrails
419
458
 
420
459
  - **Get consent before a mutation.** `sites add`, `sites delete`,