@sellable/install 0.1.218 → 0.1.220

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
@@ -35,8 +35,9 @@ Campaign creation, foundation memory, content capture/ideation, and post
35
35
  drafting run inside Claude Code or Codex, where the Sellable MCP tools and
36
36
  approval flows are available.
37
37
 
38
- Install is auth-free by default. If you do not pass a token, the agent handles
39
- Sellable sign-in on the first campaign run with a magic-link handoff.
38
+ Install is auth-free by default. The normal path is first-run login: launch a
39
+ Sellable workflow in Claude Code or Codex and the agent handles Sellable
40
+ sign-in with a browser magic-link handoff.
40
41
 
41
42
  The installer uses package stdio MCP by default:
42
43
 
@@ -64,14 +65,15 @@ verification in one path:
64
65
  curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh
65
66
  ```
66
67
 
67
- For CI/scripted installs, get a Sellable API token from:
68
+ For scripted fallback after browser login, paste the command shown by Sellable:
68
69
 
69
- ```text
70
- https://app.sellable.dev/settings
70
+ ```bash
71
+ sellable auth set <token> --workspace-id <workspace_id>
71
72
  ```
72
73
 
73
- Then pass it with `--token` / `SELLABLE_TOKEN` plus `--workspace-id` /
74
- `SELLABLE_WORKSPACE_ID`.
74
+ For CI/env-only installs, operators can still pass `--token` / `SELLABLE_TOKEN`
75
+ plus `--workspace-id` / `SELLABLE_WORKSPACE_ID`. Do not use env vars as the
76
+ primary human setup path.
75
77
 
76
78
  Auth is stored once at:
77
79
 
@@ -605,13 +605,26 @@ async function loadAuthIfPresent(opts) {
605
605
  return opts;
606
606
  }
607
607
 
608
+ function redactTokensForLog(value) {
609
+ if (Array.isArray(value)) {
610
+ return value.map((item) => redactTokensForLog(item));
611
+ }
612
+ if (value && typeof value === "object") {
613
+ return Object.fromEntries(
614
+ Object.entries(value).map(([key, item]) => [
615
+ key,
616
+ key.toLowerCase().includes("token") && typeof item === "string"
617
+ ? redact(item)
618
+ : redactTokensForLog(item),
619
+ ])
620
+ );
621
+ }
622
+ return value;
623
+ }
624
+
608
625
  function writeJson(path, data, opts) {
609
626
  if (VERBOSE) {
610
- const redacted = JSON.stringify(
611
- { ...data, token: redact(data.token) },
612
- null,
613
- 2
614
- );
627
+ const redacted = JSON.stringify(redactTokensForLog(data), null, 2);
615
628
  logVerbose(`${C.grey}Writing ${path}: ${redacted}${C.reset}`);
616
629
  }
617
630
  if (opts.dryRun) return;
@@ -628,6 +641,56 @@ function readExisting(path) {
628
641
  }
629
642
  }
630
643
 
644
+ function mergeAuthConfig(raw, auth) {
645
+ const existing =
646
+ raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
647
+ const authFields = {
648
+ token: auth.token,
649
+ activeWorkspaceId: auth.activeWorkspaceId,
650
+ apiUrl: auth.apiUrl,
651
+ };
652
+
653
+ if (existing.activeEnv && existing.environments) {
654
+ const activeEnv = existing.activeEnv;
655
+ const envConfig = existing.environments[activeEnv];
656
+ if (
657
+ !envConfig ||
658
+ typeof envConfig !== "object" ||
659
+ Array.isArray(envConfig)
660
+ ) {
661
+ throw new Error(
662
+ `Unknown active environment '${activeEnv}' in ${authPath()}`
663
+ );
664
+ }
665
+ return {
666
+ ...existing,
667
+ environments: {
668
+ ...existing.environments,
669
+ [activeEnv]: {
670
+ ...envConfig,
671
+ ...authFields,
672
+ },
673
+ },
674
+ };
675
+ }
676
+
677
+ return {
678
+ ...existing,
679
+ ...authFields,
680
+ };
681
+ }
682
+
683
+ function writeAuthSetConfig({ token, workspaceId, apiUrl, dryRun }) {
684
+ const configPath = authPath();
685
+ const raw = readExisting(configPath) || {};
686
+ const config = mergeAuthConfig(raw, {
687
+ token,
688
+ activeWorkspaceId: workspaceId || null,
689
+ apiUrl,
690
+ });
691
+ writeJson(configPath, config, { dryRun });
692
+ }
693
+
631
694
  function sellableHostEnvPath() {
632
695
  return join(homedir(), ".local", "sellable", "app-sellable-dev", ".env");
633
696
  }
@@ -925,8 +988,11 @@ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
925
988
  "mcp__sellable__save_rubrics",
926
989
  "mcp__sellable__get_campaign_table_schema",
927
990
  "mcp__sellable__select_campaign_cells",
991
+ "mcp__sellable__record_campaign_review_batch",
928
992
  "mcp__sellable__queue_campaign_cells",
929
993
  "mcp__sellable__wait_for_campaign_processing",
994
+ "mcp__sellable__refill_campaign_sends",
995
+ "mcp__sellable__resolve_campaign_fill_route",
930
996
  "mcp__sellable__fill_campaign_horizon",
931
997
  "mcp__sellable__start_campaign_message_preparation",
932
998
  "mcp__sellable__get_campaign_message_preparation_status",
@@ -3909,18 +3975,31 @@ async function main() {
3909
3975
  }
3910
3976
  const token = rawArgs[2];
3911
3977
  const authSetFlags = rawArgs.slice(3);
3912
- const dryRun = authSetFlags.includes("--dry-run");
3913
- const unknownAuthSetFlag = authSetFlags.find(
3914
- (arg) => arg !== "--dry-run"
3915
- );
3916
- if (unknownAuthSetFlag) {
3917
- console.error(`Unknown auth set option: ${unknownAuthSetFlag}`);
3978
+ let dryRun = false;
3979
+ let workspaceId = "";
3980
+ for (let i = 0; i < authSetFlags.length; i += 1) {
3981
+ const flag = authSetFlags[i];
3982
+ if (flag === "--dry-run") {
3983
+ dryRun = true;
3984
+ continue;
3985
+ }
3986
+ if (flag === "--workspace-id") {
3987
+ const value = authSetFlags[i + 1];
3988
+ if (!value || value.startsWith("--")) {
3989
+ console.error("Missing value for --workspace-id");
3990
+ process.exit(2);
3991
+ }
3992
+ workspaceId = value;
3993
+ i += 1;
3994
+ continue;
3995
+ }
3996
+ console.error(`Unknown auth set option: ${flag}`);
3918
3997
  process.exit(2);
3919
3998
  }
3920
3999
  if (!token) {
3921
4000
  console.error(
3922
- "Usage: sellable auth set <token>\n" +
3923
- "Get the token from the Sellable browser confirm page (after clicking the magic link)."
4001
+ "Usage: sellable auth set <token> [--workspace-id <id>]\n" +
4002
+ "Use this only when the Sellable browser login page shows a manual fallback command."
3924
4003
  );
3925
4004
  process.exit(2);
3926
4005
  }
@@ -3932,23 +4011,22 @@ async function main() {
3932
4011
  );
3933
4012
  process.exit(2);
3934
4013
  }
3935
- // Bypass writeAuth() its workspaceId guard early-returns on missing
3936
- // workspaceId, but on the auth-set path we don't have one yet (next MCP
3937
- // call hydrates it). Write the same shape directly.
4014
+ // Bypass writeAuth() because auth-set is a first-run browser fallback:
4015
+ // the workspace id is optional for legacy fallback pages, and existing
4016
+ // config metadata must be preserved.
3938
4017
  // Skip installSelfShim() — by definition the user has the shim already
3939
4018
  // (they invoked `sellable auth set` from it).
3940
4019
  const apiUrl = process.env.SELLABLE_API_URL || DEFAULT_API_URL;
3941
- writeJson(
3942
- authPath(),
3943
- { token, activeWorkspaceId: null, apiUrl },
3944
- { dryRun }
3945
- );
4020
+ writeAuthSetConfig({ token, workspaceId, apiUrl, dryRun });
3946
4021
  if (dryRun) {
3947
4022
  console.log(`Dry run: token would be saved to ${authPath()}`);
3948
4023
  } else {
3949
4024
  console.log(`✓ Token saved to ${authPath()}`);
3950
4025
  }
3951
4026
  console.log(` apiUrl: ${apiUrl}`);
4027
+ if (workspaceId) {
4028
+ console.log(` activeWorkspaceId: ${workspaceId}`);
4029
+ }
3952
4030
  console.log(` Continue in your agent:`);
3953
4031
  console.log(` Claude Code: /sellable:create-campaign`);
3954
4032
  console.log(` Claude Code: /sellable:foundation`);
@@ -28,6 +28,8 @@ export const REQUIRED_SELLABLE_MCP_TOOLS = [
28
28
  "get_campaign_messages_preview",
29
29
  "attach_sequence",
30
30
  "attach_recommended_sequence",
31
+ "refill_campaign_sends",
32
+ "resolve_campaign_fill_route",
31
33
  "start_campaign_message_preparation",
32
34
  "get_campaign_message_preparation_status",
33
35
  "cancel_campaign_message_preparation",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.218",
3
+ "version": "0.1.220",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {
@@ -44,8 +44,11 @@ allowed-tools:
44
44
  - mcp__sellable__save_rubrics
45
45
  - mcp__sellable__get_campaign_table_schema
46
46
  - mcp__sellable__select_campaign_cells
47
+ - mcp__sellable__record_campaign_review_batch
47
48
  - mcp__sellable__queue_campaign_cells
48
49
  - mcp__sellable__wait_for_campaign_processing
50
+ - mcp__sellable__refill_campaign_sends
51
+ - mcp__sellable__resolve_campaign_fill_route
49
52
  - mcp__sellable__fill_campaign_horizon
50
53
  - mcp__sellable__start_campaign_message_preparation
51
54
  - mcp__sellable__get_campaign_message_preparation_status
@@ -105,37 +108,54 @@ most one direct `enrich_with_prospeo` sample when row evidence is too thin.
105
108
  After filter approval, the browser should move to Filter Leads with
106
109
  `currentStep: "apply-icp-rubric"` and show template waiting/approval copy until
107
110
  the template is approved.
111
+ If the bounded filter run later returns `0/N` passes, do not immediately find
112
+ new leads. Load `references/sample-validation-loop.md`, run the zero-pass
113
+ rule-relaxability audit, then choose exactly one recovery: revise saved
114
+ rubrics, record a fresh same-source review batch, or change lead source. Change
115
+ source only when safe relaxation and same-source sampling would still fail or
116
+ would pass bad-fit rows.
108
117
  The default path stays the existing first campaign-table execution slice:
109
118
  review the normal `reviewBatchLimit:15`, approve reviewed draft rows, then move
110
- to Settings/sequence/final greenlight. Only call
111
- `fill_campaign_horizon` for explicit source-cleanup horizon-fill requests, such
112
- as "fill sends from Signal Discovery but not John Cutler posts." Run
113
- `fill_campaign_horizon({ action:"audit", ... })` first, then apply with the
114
- returned `stateRevision`. It imports/prepares at most 300 eligible non-excluded
115
- source rows in the first pass, caps prep batches at 100, skips existing rows
116
- from excluded post/author sources, and does not launch the campaign. If the
117
- user only asks for generic extra sends with no source cleanup, use
118
- `start_campaign_message_preparation` when the user explicitly asks for more
119
- prepared messages, a send count, or language like "fill up/load sends for these
120
- senders." Treat those requests as capacity-fill preparation: calculate the
121
- bounded target from sender capacity when needed, then let the preparation job
122
- queue pending `Enrich Prospect` cells, wait for ICP/rubric and Generate Message
123
- cells to cascade, and mark ready or approve only the target cohort. Do not
124
- count `checkedRows` as enriched rows; it is only the table cursor. Use
119
+ to Settings/sequence/final greenlight. For any plain post-mint fill request
120
+ such as "fill campaigns", "fill up", "refill sends", "max out sends", or
121
+ "load sends", first call `refill_campaign_sends({ mode:"plan", intent:"plain" })`.
122
+ If the user did not include `--yolo`, report the plan and stop. If the user did
123
+ include `--yolo`, call plan first, inspect blockers, then call
124
+ `refill_campaign_sends({ mode:"apply", yolo:true, planRevision, actionIds })`
125
+ using only the selected immutable action IDs from the fresh plan. Plain fill is
126
+ not an alias for `fill_campaign_horizon` or campaign creation. Keep
127
+ `resolve_campaign_fill_route`, `fill_campaign_horizon`, and
128
+ `start_campaign_message_preparation` as fallback/lower-level diagnostics only
129
+ when `refill_campaign_sends` is unavailable or when the refill command itself
130
+ returns an evergreen subplan. `fill_campaign_horizon` is evergreen-only and
131
+ must not be used for regular campaign refill. When more leads are needed, the
132
+ plan must recommend same-campaign source-ladder replenishment; do not create
133
+ warm-post-engager side campaigns, on-demand campaigns, or unrelated campaigns.
134
+ If fallback `resolve_campaign_fill_route` returns `route:"ask_create"`, ask
135
+ whether to create a normal campaign or evergreen campaigns; campaign creation is
136
+ never the default response to plain fill.
137
+ Treat active fills as capacity-fill preparation: calculate the bounded target
138
+ from sender capacity when needed, then let the preparation job queue pending
139
+ `Enrich Prospect` cells, wait for ICP/rubric and Generate Message cells to
140
+ cascade, and mark ready or approve only the target cohort. Do not count
141
+ `checkedRows` as enriched rows; it is only the table cursor. Use
125
142
  `progress.enrichedRows`, `progress.needsEnrichRows`, `activeCellCount`,
126
- `preparedMessages`, `approvedRows`, target, estimated row budget remaining, and
127
- `stopReason` to explain progress. If the user says "prepare/generate X
128
- messages", set `targetPreparedMessages:X`, omit `maxRowsToCheck`, and keep
143
+ `preparedMessages`, `approvedRows`, active preparation jobs, sender-health
144
+ blockers, target, estimated row budget remaining, and `stopReason` to explain
145
+ progress. If the user says "prepare/generate X messages", set
146
+ `targetPreparedMessages:X`, omit `maxRowsToCheck`, and keep
129
147
  `approvalMode:"mark_ready"`. The backend calibrates on at least 100 actually
130
148
  enriched rows, estimates the row budget from observed rubric/pass yield, caps
131
149
  `maxRowsToCheck` at 300, then continues in batches capped at 100 newly checked
132
150
  rows. It will not pull another row batch while the current checked batch still
133
151
  has queueable or active cells. If the user says "approve X messages", use
134
152
  `approvalMode:"approve"` but still do not launch. If the user says "schedule X
135
- sends" or asks to fill sender sends, use `approvalMode:"approve"` to approve
136
- exactly the bounded X-message cohort during preparation, then continue through
137
- sender, sequence, and final launch greenlight; the launch path must verify that
138
- bounded cohort and must not broad approve-all.
153
+ sends" or asks to fill sender sends, use `approvalMode:"approve"` only when
154
+ the user explicitly asked for approval, approve exactly the bounded X-message
155
+ cohort during preparation, then re-read scheduled counts; if scheduler-owned
156
+ cells are not present, report prepared/approved/ready awaiting scheduler
157
+ instead of success. Final launch remains a separate explicit user greenlight
158
+ and must verify that bounded cohort and must not broad approve-all.
139
159
  When approving reviewed draft rows in the campaign table, resolve the actual
140
160
  visible `Approved` cells with `select_campaign_cells({ columnRole: "approved",
141
161
  rowSelector: { type: "rowIds", rowIds } })` and `update_cell` those returned
@@ -893,7 +913,11 @@ updates.
893
913
 
894
914
  1. Call `mcp__sellable__get_auth_status({})`.
895
915
  2. If auth is not OK with `error.type === "config"` or `error.type === "auth"`,
896
- the user has not signed in yet. Run the FTUX magic-link handoff:
916
+ the user has not signed in yet. Run first-run login through the FTUX
917
+ magic-link handoff. If a browser page or tool guidance gives the user a
918
+ manual fallback, it must be
919
+ `sellable auth set <token> --workspace-id <workspace_id>`. Do not instruct
920
+ the user to hand-edit JSON auth config.
897
921
 
898
922
  a. Say to the user verbatim:
899
923
 
@@ -1081,7 +1105,11 @@ updates.
1081
1105
  rubrics and Message Drafting runs.
1082
1106
  After rubrics save, keep Filter Rules visible for approval; after approval,
1083
1107
  move to Filter Leads with `currentStep: "apply-icp-rubric"` and wait there
1084
- while Message Drafting finishes or the template is approved.
1108
+ while Message Drafting finishes or the template is approved. After template
1109
+ approval and bounded scoring, a `0/N` pass result must run the
1110
+ sample-validation zero-pass rule-relaxability audit before any lead-source
1111
+ revision; the three allowed recoveries are rubric revision, fresh
1112
+ same-source sample, or source revision.
1085
1113
  If filters are skipped, launch Message Drafting before moving to
1086
1114
  Messages/message review; updating `currentStep` to `messages` is not proof
1087
1115
  that the background worker started. Queue the bounded campaign-table