@sellable/install 0.1.215 → 0.1.217

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
@@ -31,12 +31,13 @@ agent command for launching a campaign:
31
31
  sellable create
32
32
  ```
33
33
 
34
- Campaign creation, foundation memory, content capture/ideation, post drafting,
35
- and inbox reply management run inside Claude Code or Codex, where the Sellable
36
- MCP tools and approval flows are available.
34
+ Campaign creation, foundation memory, content capture/ideation, and post
35
+ drafting run inside Claude Code or Codex, where the Sellable MCP tools and
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
 
@@ -97,20 +99,17 @@ Use the same public entrypoints in both hosts:
97
99
  - Claude Code: `/sellable:foundation`
98
100
  - Claude Code: `/sellable:content`
99
101
  - Claude Code: `/sellable:create-post`
100
- - Claude Code: `/sellable:inbox`
101
102
  - Codex: `$sellable:create-campaign`
102
103
  - Codex: `$sellable:create-ab-test`
103
104
  - Codex: `$sellable:foundation`
104
105
  - Codex: `$sellable:content`
105
106
  - Codex: `$sellable:create-post`
106
- - Codex: `$sellable:inbox`
107
107
  - Codex Desktop plugin: `sellable@sellable`
108
108
  - Codex visible skill: `Sellable Create Campaign`
109
109
  - Codex visible skill: `Sellable Create A/B Test`
110
110
  - Codex visible skill: `Sellable Foundation`
111
111
  - Codex visible skill: `Sellable Content`
112
112
  - Codex visible skill: `Sellable Create Post`
113
- - Codex visible skill: `Sellable Inbox`
114
113
  - Internal MCP workflow prompt: `create-campaign-v2`
115
114
  - Internal/backward-compatible memory prompt: `interview`
116
115
 
@@ -182,7 +182,7 @@ Options:
182
182
 
183
183
  Auth:
184
184
  Install is auth-free by default. Sign in happens on the first run of
185
- /sellable:create-campaign, /sellable:foundation, /sellable:content, /sellable:create-post, or /sellable:inbox in Claude Code or Codex,
185
+ /sellable:create-campaign, /sellable:foundation, /sellable:content, or /sellable:create-post in Claude Code or Codex,
186
186
  where the agent walks you through signup or sign-in and stores credentials
187
187
  in ~/.sellable/config.json.
188
188
 
@@ -211,7 +211,6 @@ function printCreateCommandHint() {
211
211
  { label: "Foundation", command: "/sellable:foundation" },
212
212
  { label: "Content", command: "/sellable:content" },
213
213
  { label: "Post", command: "/sellable:create-post" },
214
- { label: "Inbox", command: "/sellable:inbox" },
215
214
  ]);
216
215
  console.log("");
217
216
  console.log("");
@@ -221,7 +220,6 @@ function printCreateCommandHint() {
221
220
  { label: "Foundation", command: "$sellable:foundation" },
222
221
  { label: "Content", command: "$sellable:content" },
223
222
  { label: "Post", command: "$sellable:create-post" },
224
- { label: "Inbox", command: "$sellable:inbox" },
225
223
  ]);
226
224
  console.log("");
227
225
  console.log(` ${"─".repeat(63)}`);
@@ -421,13 +419,26 @@ async function loadAuthIfPresent(opts) {
421
419
  return opts;
422
420
  }
423
421
 
422
+ function redactTokensForLog(value) {
423
+ if (Array.isArray(value)) {
424
+ return value.map((item) => redactTokensForLog(item));
425
+ }
426
+ if (value && typeof value === "object") {
427
+ return Object.fromEntries(
428
+ Object.entries(value).map(([key, item]) => [
429
+ key,
430
+ key.toLowerCase().includes("token") && typeof item === "string"
431
+ ? redact(item)
432
+ : redactTokensForLog(item),
433
+ ])
434
+ );
435
+ }
436
+ return value;
437
+ }
438
+
424
439
  function writeJson(path, data, opts) {
425
440
  if (VERBOSE) {
426
- const redacted = JSON.stringify(
427
- { ...data, token: redact(data.token) },
428
- null,
429
- 2
430
- );
441
+ const redacted = JSON.stringify(redactTokensForLog(data), null, 2);
431
442
  logVerbose(`${C.grey}Writing ${path}: ${redacted}${C.reset}`);
432
443
  }
433
444
  if (opts.dryRun) return;
@@ -444,6 +455,56 @@ function readExisting(path) {
444
455
  }
445
456
  }
446
457
 
458
+ function mergeAuthConfig(raw, auth) {
459
+ const existing =
460
+ raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
461
+ const authFields = {
462
+ token: auth.token,
463
+ activeWorkspaceId: auth.activeWorkspaceId,
464
+ apiUrl: auth.apiUrl,
465
+ };
466
+
467
+ if (existing.activeEnv && existing.environments) {
468
+ const activeEnv = existing.activeEnv;
469
+ const envConfig = existing.environments[activeEnv];
470
+ if (
471
+ !envConfig ||
472
+ typeof envConfig !== "object" ||
473
+ Array.isArray(envConfig)
474
+ ) {
475
+ throw new Error(
476
+ `Unknown active environment '${activeEnv}' in ${authPath()}`
477
+ );
478
+ }
479
+ return {
480
+ ...existing,
481
+ environments: {
482
+ ...existing.environments,
483
+ [activeEnv]: {
484
+ ...envConfig,
485
+ ...authFields,
486
+ },
487
+ },
488
+ };
489
+ }
490
+
491
+ return {
492
+ ...existing,
493
+ ...authFields,
494
+ };
495
+ }
496
+
497
+ function writeAuthSetConfig({ token, workspaceId, apiUrl, dryRun }) {
498
+ const configPath = authPath();
499
+ const raw = readExisting(configPath) || {};
500
+ const config = mergeAuthConfig(raw, {
501
+ token,
502
+ activeWorkspaceId: workspaceId || null,
503
+ apiUrl,
504
+ });
505
+ writeJson(configPath, config, { dryRun });
506
+ }
507
+
447
508
  function sellableHostEnvPath() {
448
509
  return join(homedir(), ".local", "sellable", "app-sellable-dev", ".env");
449
510
  }
@@ -743,6 +804,7 @@ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
743
804
  "mcp__sellable__select_campaign_cells",
744
805
  "mcp__sellable__queue_campaign_cells",
745
806
  "mcp__sellable__wait_for_campaign_processing",
807
+ "mcp__sellable__fill_campaign_horizon",
746
808
  "mcp__sellable__start_campaign_message_preparation",
747
809
  "mcp__sellable__get_campaign_message_preparation_status",
748
810
  "mcp__sellable__cancel_campaign_message_preparation",
@@ -1755,67 +1817,6 @@ then retry \`get_subskill_prompt\`.
1755
1817
  `, host, "create-post");
1756
1818
  }
1757
1819
 
1758
- function inboxSkillMd(host = "shared") {
1759
- return stampInstalledHost(`---
1760
- name: inbox
1761
- description: Search Sellable inbox threads, review reply eligibility, and manage approval-gated LinkedIn replies.
1762
- allowed-tools:
1763
- - mcp__sellable__get_auth_status
1764
- - mcp__sellable__search_inbox_threads
1765
- - mcp__sellable__get_inbox_thread
1766
- - mcp__sellable__check_inbox_reply_eligibility
1767
- - mcp__sellable__update_inbox_draft
1768
- - mcp__sellable__send_inbox_draft
1769
- - mcp__sellable__send_inbox_manual_reply
1770
- ---
1771
-
1772
- # Sellable Inbox
1773
-
1774
- Use this as the customer-facing entrypoint for Sellable inbox reply management.
1775
- It can search LinkedIn inbox threads, load thread history, check product reply
1776
- eligibility, update an existing draft, send an approved draft, or send one exact
1777
- manual reply.
1778
-
1779
- ## Bootstrap
1780
-
1781
- MCP tool access is required. First call \`mcp__sellable__get_auth_status({})\`.
1782
- Do not inspect repo files, run shell commands, use \`npm\`, \`node\`, local
1783
- harness scripts, browser automation, or product API routes to emulate this
1784
- workflow.
1785
-
1786
- If the Sellable MCP tool is unavailable, stop and say this is a Codex
1787
- install/reload problem. Tell the user to run
1788
- \`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\`, fully quit and reopen Codex
1789
- Desktop, then start a new thread.
1790
-
1791
- ## Read Workflow
1792
-
1793
- 1. Call \`mcp__sellable__get_auth_status({})\`.
1794
- 2. Use \`mcp__sellable__search_inbox_threads({ limit, search?, filter?, status?, senderId?, classification?, includeReplyEligibility? })\` to find candidate threads.
1795
- 3. Use \`mcp__sellable__get_inbox_thread({ threadId })\` before summarizing or drafting.
1796
- 4. Use \`mcp__sellable__check_inbox_reply_eligibility({ threadId })\` before proposing any reply send path.
1797
-
1798
- ## Approval Gates
1799
-
1800
- Never call \`mcp__sellable__update_inbox_draft\`,
1801
- \`mcp__sellable__send_inbox_draft\`, or
1802
- \`mcp__sellable__send_inbox_manual_reply\` until the user has explicitly
1803
- approved all of:
1804
-
1805
- - workspace/account context
1806
- - sender
1807
- - thread or recipient
1808
- - exact body
1809
- - exact tool to call
1810
- - expected side effect
1811
-
1812
- Draft edits only edit an existing draft. They do not create a new reply draft.
1813
- Send tools support one thread at a time; do not batch sends. If any approval,
1814
- snapshot, hash, version, eligibility, or idempotency value is missing or stale,
1815
- stop and re-read the thread instead of guessing.
1816
- `, host, "inbox");
1817
- }
1818
-
1819
1820
  function createCampaignSoulMd() {
1820
1821
  return `# Sellable Campaign GTM Engineer Soul
1821
1822
 
@@ -2037,12 +2038,6 @@ function codexPluginSkills() {
2037
2038
  description: "Capture ideas and draft LinkedIn posts in your voice",
2038
2039
  skillMd: createPostSkillMd(),
2039
2040
  },
2040
- {
2041
- dir: "sellable-inbox",
2042
- displayName: "Sellable Inbox",
2043
- description: "Search and manage approval-gated LinkedIn inbox replies",
2044
- skillMd: inboxSkillMd(),
2045
- },
2046
2041
  ];
2047
2042
  }
2048
2043
 
@@ -2268,23 +2263,6 @@ function claudeCommands() {
2268
2263
  argumentHint: "[post idea or source material]",
2269
2264
  skillMd: createPostSkillMd(),
2270
2265
  },
2271
- {
2272
- name: "inbox",
2273
- title: "Inbox",
2274
- filename: join("sellable", "inbox.md"),
2275
- description: "Search and manage approval-gated Sellable inbox replies.",
2276
- argumentHint: "[thread, sender, recipient, or reply goal]",
2277
- skillMd: inboxSkillMd(),
2278
- allowedTools: [
2279
- "mcp__sellable__get_auth_status",
2280
- "mcp__sellable__search_inbox_threads",
2281
- "mcp__sellable__get_inbox_thread",
2282
- "mcp__sellable__check_inbox_reply_eligibility",
2283
- "mcp__sellable__update_inbox_draft",
2284
- "mcp__sellable__send_inbox_draft",
2285
- "mcp__sellable__send_inbox_manual_reply",
2286
- ],
2287
- },
2288
2266
  ].map((command) => ({
2289
2267
  ...command,
2290
2268
  content: claudeCommandMd(command),
@@ -3508,7 +3486,6 @@ function printNextSteps(installedHosts, authReused) {
3508
3486
  { label: "Foundation", command: "/sellable:foundation" },
3509
3487
  { label: "Content", command: "/sellable:content" },
3510
3488
  { label: "Post", command: "/sellable:create-post" },
3511
- { label: "Inbox", command: "/sellable:inbox" },
3512
3489
  ]);
3513
3490
  console.log("");
3514
3491
  console.log("");
@@ -3519,7 +3496,6 @@ function printNextSteps(installedHosts, authReused) {
3519
3496
  { label: "Foundation", command: "$sellable:foundation" },
3520
3497
  { label: "Content", command: "$sellable:content" },
3521
3498
  { label: "Post", command: "$sellable:create-post" },
3522
- { label: "Inbox", command: "$sellable:inbox" },
3523
3499
  ]);
3524
3500
  console.log("");
3525
3501
  }
@@ -3752,18 +3728,31 @@ async function main() {
3752
3728
  }
3753
3729
  const token = rawArgs[2];
3754
3730
  const authSetFlags = rawArgs.slice(3);
3755
- const dryRun = authSetFlags.includes("--dry-run");
3756
- const unknownAuthSetFlag = authSetFlags.find(
3757
- (arg) => arg !== "--dry-run"
3758
- );
3759
- if (unknownAuthSetFlag) {
3760
- console.error(`Unknown auth set option: ${unknownAuthSetFlag}`);
3731
+ let dryRun = false;
3732
+ let workspaceId = "";
3733
+ for (let i = 0; i < authSetFlags.length; i += 1) {
3734
+ const flag = authSetFlags[i];
3735
+ if (flag === "--dry-run") {
3736
+ dryRun = true;
3737
+ continue;
3738
+ }
3739
+ if (flag === "--workspace-id") {
3740
+ const value = authSetFlags[i + 1];
3741
+ if (!value || value.startsWith("--")) {
3742
+ console.error("Missing value for --workspace-id");
3743
+ process.exit(2);
3744
+ }
3745
+ workspaceId = value;
3746
+ i += 1;
3747
+ continue;
3748
+ }
3749
+ console.error(`Unknown auth set option: ${flag}`);
3761
3750
  process.exit(2);
3762
3751
  }
3763
3752
  if (!token) {
3764
3753
  console.error(
3765
- "Usage: sellable auth set <token>\n" +
3766
- "Get the token from the Sellable browser confirm page (after clicking the magic link)."
3754
+ "Usage: sellable auth set <token> [--workspace-id <id>]\n" +
3755
+ "Use this only when the Sellable browser login page shows a manual fallback command."
3767
3756
  );
3768
3757
  process.exit(2);
3769
3758
  }
@@ -3775,34 +3764,31 @@ async function main() {
3775
3764
  );
3776
3765
  process.exit(2);
3777
3766
  }
3778
- // Bypass writeAuth() its workspaceId guard early-returns on missing
3779
- // workspaceId, but on the auth-set path we don't have one yet (next MCP
3780
- // call hydrates it). Write the same shape directly.
3767
+ // Bypass writeAuth() because auth-set is a first-run browser fallback:
3768
+ // the workspace id is optional for legacy fallback pages, and existing
3769
+ // config metadata must be preserved.
3781
3770
  // Skip installSelfShim() — by definition the user has the shim already
3782
3771
  // (they invoked `sellable auth set` from it).
3783
3772
  const apiUrl = process.env.SELLABLE_API_URL || DEFAULT_API_URL;
3784
- writeJson(
3785
- authPath(),
3786
- { token, activeWorkspaceId: null, apiUrl },
3787
- { dryRun }
3788
- );
3773
+ writeAuthSetConfig({ token, workspaceId, apiUrl, dryRun });
3789
3774
  if (dryRun) {
3790
3775
  console.log(`Dry run: token would be saved to ${authPath()}`);
3791
3776
  } else {
3792
3777
  console.log(`✓ Token saved to ${authPath()}`);
3793
3778
  }
3794
3779
  console.log(` apiUrl: ${apiUrl}`);
3780
+ if (workspaceId) {
3781
+ console.log(` activeWorkspaceId: ${workspaceId}`);
3782
+ }
3795
3783
  console.log(` Continue in your agent:`);
3796
3784
  console.log(` Claude Code: /sellable:create-campaign`);
3797
3785
  console.log(` Claude Code: /sellable:foundation`);
3798
3786
  console.log(` Claude Code: /sellable:content`);
3799
3787
  console.log(` Claude Code: /sellable:create-post`);
3800
- console.log(` Claude Code: /sellable:inbox`);
3801
3788
  console.log(` Codex: $sellable:create-campaign`);
3802
3789
  console.log(` Codex: $sellable:foundation`);
3803
3790
  console.log(` Codex: $sellable:content`);
3804
3791
  console.log(` Codex: $sellable:create-post`);
3805
- console.log(` Codex: $sellable:inbox`);
3806
3792
  process.exit(0);
3807
3793
  }
3808
3794
  if (rawArgs[0] === "prefs") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.215",
3
+ "version": "0.1.217",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {
@@ -46,6 +46,7 @@ allowed-tools:
46
46
  - mcp__sellable__select_campaign_cells
47
47
  - mcp__sellable__queue_campaign_cells
48
48
  - mcp__sellable__wait_for_campaign_processing
49
+ - mcp__sellable__fill_campaign_horizon
49
50
  - mcp__sellable__start_campaign_message_preparation
50
51
  - mcp__sellable__get_campaign_message_preparation_status
51
52
  - mcp__sellable__cancel_campaign_message_preparation
@@ -107,6 +108,13 @@ the template is approved.
107
108
  The default path stays the existing first campaign-table execution slice:
108
109
  review the normal `reviewBatchLimit:15`, approve reviewed draft rows, then move
109
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
110
118
  `start_campaign_message_preparation` when the user explicitly asks for more
111
119
  prepared messages, a send count, or language like "fill up/load sends for these
112
120
  senders." Treat those requests as capacity-fill preparation: calculate the
@@ -120,8 +128,9 @@ count `checkedRows` as enriched rows; it is only the table cursor. Use
120
128
  messages", set `targetPreparedMessages:X`, omit `maxRowsToCheck`, and keep
121
129
  `approvalMode:"mark_ready"`. The backend calibrates on at least 100 actually
122
130
  enriched rows, estimates the row budget from observed rubric/pass yield, caps
123
- `maxRowsToCheck` at 2500, then adapts later batches up to 250 rows while
124
- recalculating yield. If the user says "approve X messages", use
131
+ `maxRowsToCheck` at 300, then continues in batches capped at 100 newly checked
132
+ rows. It will not pull another row batch while the current checked batch still
133
+ has queueable or active cells. If the user says "approve X messages", use
125
134
  `approvalMode:"approve"` but still do not launch. If the user says "schedule X
126
135
  sends" or asks to fill sender sends, use `approvalMode:"approve"` to approve
127
136
  exactly the bounded X-message cohort during preparation, then continue through
@@ -884,7 +893,11 @@ updates.
884
893
 
885
894
  1. Call `mcp__sellable__get_auth_status({})`.
886
895
  2. If auth is not OK with `error.type === "config"` or `error.type === "auth"`,
887
- the user has not signed in yet. Run the FTUX magic-link handoff:
896
+ the user has not signed in yet. Run first-run login through the FTUX
897
+ magic-link handoff. If a browser page or tool guidance gives the user a
898
+ manual fallback, it must be
899
+ `sellable auth set <token> --workspace-id <workspace_id>`. Do not instruct
900
+ the user to hand-edit JSON auth config.
888
901
 
889
902
  a. Say to the user verbatim:
890
903