@sprid/cli 0.1.14 → 0.1.15

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/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  sprid follows semver: a breaking change to a command's arguments, its output
4
4
  shape or its exit codes is a major release.
5
5
 
6
+ ## 0.1.15 - 2026-09-27
7
+
8
+ - `sprid marketing-review save --app <slug> --file <review.json>` saves a
9
+ finished review's verdict and ranked agenda to the app, where the Reviews
10
+ page, the Monday mail and the next review read it. The full report stays in
11
+ the repo. `sprid marketing-review history --app <slug>` lists past reviews.
12
+
6
13
  ## 0.1.14 - 2026-09-27
7
14
 
8
15
  - `sprid docs posthog` explains how to keep your own testing out of the numbers:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprid/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -7,9 +7,9 @@ import { saveEvidence } from '../evidence.mjs';
7
7
 
8
8
  export async function marketingReview(ctx) {
9
9
  const local = readLocalApp(ctx.cwd)?.data;
10
- if ((local?.enabled === false || local?.review?.enabled === false) && !['context', 'collection', 'evidence'].includes(ctx.positionals[0])) throw new UsageError(`Review paused: ${local.review?.disabledReason || local.disabledReason || local.slug}`);
11
- if (ctx.positionals.length > 1 || (ctx.positionals.length && !['query', 'capabilities', 'context', 'collection', 'evidence'].includes(ctx.positionals[0]))) throw new UsageError('Use sprid marketing-review [capabilities | query | context | collection | evidence]');
12
- if (['context', 'collection', 'evidence'].includes(ctx.positionals[0])) return reviewSettings(ctx);
10
+ if ((local?.enabled === false || local?.review?.enabled === false) && !['context', 'collection', 'evidence', 'history'].includes(ctx.positionals[0])) throw new UsageError(`Review paused: ${local.review?.disabledReason || local.disabledReason || local.slug}`);
11
+ if (ctx.positionals.length > 1 || (ctx.positionals.length && !['query', 'capabilities', 'context', 'collection', 'evidence', 'history', 'save'].includes(ctx.positionals[0]))) throw new UsageError('Use sprid marketing-review [capabilities | query | context | collection | evidence | history | save]');
12
+ if (['context', 'collection', 'evidence', 'history', 'save'].includes(ctx.positionals[0])) return reviewSettings(ctx);
13
13
  if (ctx.positionals[0] === 'capabilities' || (ctx.positionals[0] === 'query' && ctx.flags.source)) return connectedQuery(ctx);
14
14
  if (ctx.positionals[0] === 'query') {
15
15
  if (!ctx.flags['query-file']) throw new UsageError('sprid marketing-review query --query-file <file.sql> --app <slug>');
@@ -88,6 +88,19 @@ async function reviewSettings(ctx) {
88
88
  ctx.out({ ...result, localEvidence: saveEvidence(ctx.cwd, 'snapshot', result) });
89
89
  return 0;
90
90
  }
91
+ if (ctx.positionals[0] === 'history') {
92
+ ctx.out(await ctx.api().get(`/api/marketing-review/history?${new URLSearchParams({ workspaceId: String(wid), app: profile.slug })}`));
93
+ return 0;
94
+ }
95
+ if (ctx.positionals[0] === 'save') {
96
+ if (!ctx.flags.file) throw new UsageError('Use marketing-review save --app <slug> --file <review.json> with {verdict, agenda:[{title, why}], period:{start,end}, reportPath}');
97
+ let input;
98
+ try { input = JSON.parse(readFileSync(resolve(ctx.cwd, ctx.flags.file), 'utf8')); } catch { throw new UsageError('Review file must contain JSON with a verdict and a ranked agenda'); }
99
+ if (typeof input?.verdict !== 'string' || !input.verdict.trim()) throw new UsageError('Review file needs a verdict');
100
+ const response = await ctx.api().raw('POST', `/api/marketing-review/history?${new URLSearchParams({ workspaceId: String(wid), app: profile.slug })}`, { surface: 'repo', ...input });
101
+ ctx.out(response.data);
102
+ return response.status >= 200 && response.status < 300 ? 0 : 1;
103
+ }
91
104
  if (ctx.positionals[0] === 'collection') {
92
105
  if (!['true', 'false'].includes(ctx.flags.enabled)) throw new UsageError('Use marketing-review collection --enabled true|false --app <slug>');
93
106
  ctx.out(await patchProfile(ctx, wid, profile.id, { metricsCollectionEnabled: ctx.flags.enabled === 'true' }));
@@ -92,6 +92,8 @@ export const COMMAND_GROUPS = [
92
92
  ['marketing-review', 'sprid marketing-review --view summary|full [--sources gsc,posthog] [--refresh|--cached-only]', 'Summary is a compact entry point; full retains evidence. Exact-window snapshots name freshness. Refresh forces provider reads; cached-only makes none. Daily collection refreshes connected sources automatically for active plans.'],
93
93
  ['marketing-review', 'sprid marketing-review evidence --app <slug> --id <evidence.id>', 'Read the exact app-scoped source snapshot without provider calls, including after key rotation. Saved privately for 90 days; this command also archives the receipt locally.'],
94
94
  ['marketing-review', 'sprid marketing-review context --app <slug> [--file <context.json>]', 'Read shared review memory, or save {baseRevision,context}. Context holds definitions, investigations, decisions, corrections and release references. Concurrent changes return a conflict without overwriting. Explicitly select context to share; never upload keys or raw customer records.'],
95
+ ['marketing-review', 'sprid marketing-review save --app <slug> --file <review.json>', 'Save a finished review to the app: {verdict, agenda:[{title, why}], period:{start,end}, reportPath}. Verdict and agenda only; the full report stays in the repo. A second save the same day replaces that day’s review.'],
96
+ ['marketing-review', 'sprid marketing-review history --app <slug>', 'Past saved reviews, newest first, and when the review numbers were last read.'],
95
97
  ['marketing-review', 'sprid marketing-review collection --app <slug> --enabled true|false', 'Enable or pause daily connected-source collection. Stored evidence remains readable; this grants no publishing or model-spending permission.'],
96
98
  ['marketing-review', 'sprid marketing-review capabilities --app <slug> [--source <source>]', 'Discover read operations, parameter schemas, examples and missing setup for every connected source. Configuration is not a live credential check.'],
97
99
  ['marketing-review', 'sprid marketing-review query --app <slug> --source <source> --operation <operation> [--params-file <params.json>]', 'Run a discovered read through Sprid-held credentials. Parameters are a JSON object; resource IDs come from the App Profile. Continue with returned next.params and keep coverage/truncation in your report.'],
@@ -1614,8 +1614,8 @@ export const CONTENT_GUIDES = [
1614
1614
  "title": "Review marketing from connected evidence",
1615
1615
  "summary": "Investigate acquisition, activation, retention and revenue through the services connected to Sprid, and say plainly what evidence is missing.",
1616
1616
  "url": "https://sprid.studio/docs/marketing-review",
1617
- "markdown": "# Review marketing from connected evidence\n\n**What this guide does:** Investigate acquisition, activation, retention and revenue through the services connected to Sprid, and say plainly what evidence is missing.\n\n## Start with the question\n\nName the app, the date range and the decision the review should inform.\n\n1. `list_apps` and `list_app_profiles` to find the app.\n2. `get_marketing_review_context` for shared definitions, required investigations, earlier decisions and corrections.\n3. `get_marketing_review` for the app over two comparable periods. The summary links to full evidence per source (`view: \"full\", sources: [<source>]`); omit `sources` to include social history, store reviews and milestones.\n4. Re-read an exact saved packet with `get_marketing_review_evidence` and its `evidence.id` (CLI: `sprid marketing-review evidence --id <id> --app <slug>`). It still works after a connection changes.\n\n`refresh: true` asks for live reads; `cachedOnly: true` reads only stored evidence. Keep returned errors, coverage, population definitions and sample counts in the report.\n\nIn a browser chat there is no repository. For a question about a shipped change, ask for a release summary or public changelog and label it as supplied. Never claim to have read a repository or private database. Local users can add repo diffs and database reads through their own tools.\n\n## Follow the evidence\n\n`list_marketing_queries` lists supported reads and their schemas; `query_marketing_source` runs them with Sprid’s saved credentials. Follow pagination and keep truncation visible. `get_documentation` with `metrics` covers definitions and refused calculations; with `queries`, source-specific reads.\n\n- Keep acquisition apart from activation, and people apart from events. A post impression is not an install; a download is not an activated user.\n- Pinterest Pins resurface for months: compare them at equal ages (30, 60 and 90 days). Saves are interest on Pinterest and outbound clicks are people leaving it; neither is a site session or an install. The `pinterest` source has `posts` only, with no comments. See [Pinterest content and publishing](pinterest.md).\n- Compare equal windows and comparable populations.\n- No onboarding events means recommending instrumentation, not guessing a funnel.\n- Keep revenue sources separate until the overlap check says they can be added.\n\nFor each finding, test another explanation: tracking changes, traffic exclusions, attribution window, seasonality, a different audience mix. A before/after comparison doesn’t prove cause. If a difference is within normal variation or the sample is small, say so and don’t rank it.\n\n## Report and save\n\nLead with the decision, then the evidence, the limits and what would change the answer. Keep failed reads apart from reads that returned nothing. `next_actions` gives the follow-up. Check [remaining setup](../../../references/setup-continuation.md#keep-setup-current), including the saved app icon, voice and relevant connections. Recommend the next useful integration step without repeating completed or explicitly declined setup. A missing credential goes to the secure App Profile form or a CLI key-file command; no provider MCP is needed for a supported query.\n\n- **Shared context:** when authorized, save non-secret definitions, investigations, decisions, corrections and release references with `save_marketing_review_context`, sending `baseRevision` from the latest read. A conflict returns both versions; reconcile before retrying. Saved context is a claim with references, not verification. CLI: `sprid marketing-review context --app <slug>` reads, `--file <context.json>` imports `{baseRevision,context}`. Private notes stay local.\n- **Changes:** log confirmed product or marketing changes with `add_event`, naming the areas they affect and why.\n- **The review itself:** save it as an app-scoped `kind: \"note\"` event with the date, conclusion, evidence references, coverage and next hypothesis in `meta`. Other clients read it with `list_events`.\n\nNever store secrets or signed attachment URLs in any of these.\n\n## Collection and coverage\n\n- Sprid refreshes two rolling 30-day windows daily for configured services on active plans, and re-reads late provider exports. No model calls, publishing or paid social reads. Other windows are read on demand.\n- Pause with the App Profile’s `metricsCollectionEnabled: false` or `sprid marketing-review collection --enabled false`. Stored evidence stays readable.\n- Snapshots are private to the app, tied to its configuration and kept 90 days. Save local evidence exports for a lasting archive.\n- Missing days stay unknown. Incomplete store or Search Console windows get no percentage change. Never sum daily unique people into a monthly count.\n- `configuration_only` means identifiers and a key are saved, not that access works. Errors return a safe code, the HTTP status when known, whether to retry and the next step. Retry transient errors before recommending a reconnect. Unsupported metrics and provider privacy thresholds are stated as limits.\n",
1618
- "revision": "f4339d5f9f97297b"
1617
+ "markdown": "# Review marketing from connected evidence\n\n**What this guide does:** Investigate acquisition, activation, retention and revenue through the services connected to Sprid, and say plainly what evidence is missing.\n\n## Start with the question\n\nName the app, the date range and the decision the review should inform.\n\n1. `list_apps` and `list_app_profiles` to find the app.\n2. `get_marketing_review_context` for shared definitions, required investigations, earlier decisions and corrections.\n3. `get_marketing_review` for the app over two comparable periods. The summary links to full evidence per source (`view: \"full\", sources: [<source>]`); omit `sources` to include social history, store reviews and milestones.\n4. Re-read an exact saved packet with `get_marketing_review_evidence` and its `evidence.id` (CLI: `sprid marketing-review evidence --id <id> --app <slug>`). It still works after a connection changes.\n\n`refresh: true` asks for live reads; `cachedOnly: true` reads only stored evidence. Keep returned errors, coverage, population definitions and sample counts in the report.\n\nIn a browser chat there is no repository. For a question about a shipped change, ask for a release summary or public changelog and label it as supplied. Never claim to have read a repository or private database. Local users can add repo diffs and database reads through their own tools.\n\n## Follow the evidence\n\n`list_marketing_queries` lists supported reads and their schemas; `query_marketing_source` runs them with Sprid’s saved credentials. Follow pagination and keep truncation visible. `get_documentation` with `metrics` covers definitions and refused calculations; with `queries`, source-specific reads.\n\n- Keep acquisition apart from activation, and people apart from events. A post impression is not an install; a download is not an activated user.\n- Pinterest Pins resurface for months: compare them at equal ages (30, 60 and 90 days). Saves are interest on Pinterest and outbound clicks are people leaving it; neither is a site session or an install. The `pinterest` source has `posts` only, with no comments. See [Pinterest content and publishing](pinterest.md).\n- Compare equal windows and comparable populations.\n- No onboarding events means recommending instrumentation, not guessing a funnel.\n- Keep revenue sources separate until the overlap check says they can be added.\n\nFor each finding, test another explanation: tracking changes, traffic exclusions, attribution window, seasonality, a different audience mix. A before/after comparison doesn’t prove cause. If a difference is within normal variation or the sample is small, say so and don’t rank it.\n\n## Report and save\n\nLead with the decision, then the evidence, the limits and what would change the answer. Keep failed reads apart from reads that returned nothing. `next_actions` gives the follow-up. Check [remaining setup](../../../references/setup-continuation.md#keep-setup-current), including the saved app icon, voice and relevant connections. Recommend the next useful integration step without repeating completed or explicitly declined setup. A missing credential goes to the secure App Profile form or a CLI key-file command; no provider MCP is needed for a supported query.\n\n- **Shared context:** when authorized, save non-secret definitions, investigations, decisions, corrections and release references with `save_marketing_review_context`, sending `baseRevision` from the latest read. A conflict returns both versions; reconcile before retrying. Saved context is a claim with references, not verification. CLI: `sprid marketing-review context --app <slug>` reads, `--file <context.json>` imports `{baseRevision,context}`. Private notes stay local.\n- **Changes:** log confirmed product or marketing changes with `add_event`, naming the areas they affect and why.\n- **The review itself:** end every review with `save_marketing_review {profile, verdict, agenda: [{title, why}], period: {start, end}, surface: \"chat\"}`: the commercial verdict in a sentence or two and the ranked agenda, most consequential first. It appears on the app’s Reviews page in Sprid and is where the next review starts; `list_marketing_reviews` reads past ones. Send the verdict and agenda only, never the full report or raw customer rows. A second save the same day replaces that day’s review.\n\nNever store secrets or signed attachment URLs in any of these.\n\n## Collection and coverage\n\n- Sprid refreshes two rolling 30-day windows daily for configured services on active plans, and re-reads late provider exports. No model calls, publishing or paid social reads. Other windows are read on demand.\n- Pause with the App Profile’s `metricsCollectionEnabled: false` or `sprid marketing-review collection --enabled false`. Stored evidence stays readable.\n- Snapshots are private to the app, tied to its configuration and kept 90 days. Save local evidence exports for a lasting archive.\n- Missing days stay unknown. Incomplete store or Search Console windows get no percentage change. Never sum daily unique people into a monthly count.\n- `configuration_only` means identifiers and a key are saved, not that access works. Errors return a safe code, the HTTP status when known, whether to retry and the next step. Retry transient errors before recommending a reconnect. Unsupported metrics and provider privacy thresholds are stated as limits.\n",
1618
+ "revision": "997462410e74e207"
1619
1619
  },
1620
1620
  {
1621
1621
  "id": "traffic",