@sellable/install 0.1.208 → 0.1.209

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.
@@ -136,13 +136,22 @@ function usage() {
136
136
  return `Sellable agent installer
137
137
 
138
138
  Usage:
139
+ curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh
139
140
  sellable create
141
+ sellable prefs set prompt-sharing true|false
142
+ sellable setup claude-permissions --allow-common-setup
140
143
  sellable-install [options]
144
+
145
+ Advanced fallback:
141
146
  npx -y @sellable/install@latest -- [options]
142
147
 
143
148
  Commands:
144
149
  create Show how to launch public Sellable workflows.
145
150
  auth set <token> Save a Sellable API token for first-run auth.
151
+ prefs set prompt-sharing true|false
152
+ Save the ask-first prompt-sharing preference.
153
+ setup claude-permissions --allow-common-setup
154
+ Add common Sellable Bash permissions to Claude settings.
146
155
  uninstall Remove Sellable host config and installed artifacts.
147
156
 
148
157
  Options:
@@ -427,6 +436,83 @@ function readExisting(path) {
427
436
  }
428
437
  }
429
438
 
439
+ function sellableHostEnvPath() {
440
+ return join(homedir(), ".local", "sellable", "app-sellable-dev", ".env");
441
+ }
442
+
443
+ function writePromptSharingPreference(value, opts = { dryRun: false }) {
444
+ const normalized = String(value || "").trim().toLowerCase();
445
+ if (normalized !== "true" && normalized !== "false") {
446
+ throw new Error("prompt-sharing must be true or false");
447
+ }
448
+ const envPath = sellableHostEnvPath();
449
+ const key = "SELLABLE_SHARE_SESSION_USER_PROMPTS";
450
+ const line = `${key}=${normalized}`;
451
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
452
+ const lines = existing
453
+ .split(/\r?\n/)
454
+ .filter((existingLine) => existingLine.trim() !== "");
455
+ let replaced = false;
456
+ const nextLines = lines.map((existingLine) => {
457
+ if (existingLine.startsWith(`${key}=`)) {
458
+ replaced = true;
459
+ return line;
460
+ }
461
+ return existingLine;
462
+ });
463
+ if (!replaced) nextLines.push(line);
464
+
465
+ if (!opts.dryRun) {
466
+ mkdirSync(dirname(envPath), { recursive: true, mode: 0o700 });
467
+ writeFileSync(envPath, `${nextLines.join("\n")}\n`, { mode: 0o600 });
468
+ }
469
+ return envPath;
470
+ }
471
+
472
+ const CLAUDE_COMMON_SETUP_ALLOW = [
473
+ "Bash(sellable *)",
474
+ "Bash(export PATH=$HOME/.local/bin:$PATH && sellable *)",
475
+ ];
476
+
477
+ function claudeSettingsPath() {
478
+ return join(homedir(), ".claude", "settings.json");
479
+ }
480
+
481
+ function allowClaudeCommonSetup(opts = { dryRun: false }) {
482
+ const settingsPath = claudeSettingsPath();
483
+ let settings = {};
484
+ if (existsSync(settingsPath)) {
485
+ try {
486
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
487
+ } catch (err) {
488
+ throw new Error(
489
+ `Could not parse ${settingsPath}: ${
490
+ err instanceof Error ? err.message : String(err)
491
+ }`
492
+ );
493
+ }
494
+ }
495
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
496
+ settings = {};
497
+ }
498
+ const permissions =
499
+ settings.permissions &&
500
+ typeof settings.permissions === "object" &&
501
+ !Array.isArray(settings.permissions)
502
+ ? settings.permissions
503
+ : {};
504
+ const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
505
+ const nextAllow = [...allow];
506
+ for (const entry of CLAUDE_COMMON_SETUP_ALLOW) {
507
+ if (!nextAllow.includes(entry)) nextAllow.push(entry);
508
+ }
509
+ settings.permissions = { ...permissions, allow: nextAllow };
510
+ if (!opts.dryRun) {
511
+ writeJson(settingsPath, settings, opts);
512
+ }
513
+ return settingsPath;
514
+ }
515
+
430
516
  function codexHome() {
431
517
  return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
432
518
  }
@@ -1868,12 +1954,7 @@ function legacyClaudeCustomAgents() {
1868
1954
  }
1869
1955
 
1870
1956
  function legacyClaudeCommands() {
1871
- return [
1872
- {
1873
- name: "sellable-create-campaign",
1874
- filename: join("sellable", "create-campaign.md"),
1875
- },
1876
- ];
1957
+ return [];
1877
1958
  }
1878
1959
 
1879
1960
  function tomlArray(values) {
@@ -1930,6 +2011,72 @@ function claudeCustomAgents() {
1930
2011
  }));
1931
2012
  }
1932
2013
 
2014
+ function stripFrontmatter(markdown) {
2015
+ return String(markdown).replace(/^---\n[\s\S]*?\n---\n?/, "").trimStart();
2016
+ }
2017
+
2018
+ function claudeCommandMd(command) {
2019
+ const allowedTools =
2020
+ command.allowedTools && command.allowedTools.length > 0
2021
+ ? `allowed-tools:\n${allowedToolsYaml(command.allowedTools)}\n`
2022
+ : "";
2023
+ return `---
2024
+ name: sellable:${command.name}
2025
+ description: ${yamlString(command.description)}
2026
+ argument-hint: ${yamlString(command.argumentHint || "[instructions]")}
2027
+ ${allowedTools}---
2028
+
2029
+ # Sellable ${command.title}
2030
+
2031
+ This Claude Code command is the customer-facing entrypoint for
2032
+ \`/sellable:${command.name}\`.
2033
+
2034
+ ${stripFrontmatter(command.skillMd)}
2035
+ `;
2036
+ }
2037
+
2038
+ function claudeCommands() {
2039
+ return [
2040
+ {
2041
+ name: "create-campaign",
2042
+ title: "Create Campaign",
2043
+ filename: join("sellable", "create-campaign.md"),
2044
+ description: "Create a Sellable campaign through the approval-gated workflow.",
2045
+ argumentHint: "[campaign goal, company, or offer]",
2046
+ skillMd: createCampaignSkillMd(),
2047
+ allowedTools: ["AskUserQuestion", "Task", ...CREATE_CAMPAIGN_ALLOWED_TOOLS],
2048
+ },
2049
+ {
2050
+ name: "foundation",
2051
+ title: "Foundation",
2052
+ filename: join("sellable", "foundation.md"),
2053
+ description: FOUNDATION_SKILL_DESCRIPTION,
2054
+ argumentHint: "[founder/company context]",
2055
+ skillMd: foundationSkillMd(),
2056
+ },
2057
+ {
2058
+ name: "content",
2059
+ title: "Content",
2060
+ filename: join("sellable", "content.md"),
2061
+ description:
2062
+ "Add transcripts and rough ideas, cluster themes, and hand post requests to create-post.",
2063
+ argumentHint: "[idea, transcript, or content goal]",
2064
+ skillMd: contentSkillMd(),
2065
+ },
2066
+ {
2067
+ name: "create-post",
2068
+ title: "Create Post",
2069
+ filename: join("sellable", "create-post.md"),
2070
+ description: "Capture ideas and draft LinkedIn posts in the user's voice.",
2071
+ argumentHint: "[post idea or source material]",
2072
+ skillMd: createPostSkillMd(),
2073
+ },
2074
+ ].map((command) => ({
2075
+ ...command,
2076
+ content: claudeCommandMd(command),
2077
+ }));
2078
+ }
2079
+
1933
2080
  function writeCodexPluginSkills(pluginRoot, opts) {
1934
2081
  const skills = codexPluginSkills();
1935
2082
  const currentSkillDirs = new Set(skills.map((skill) => skill.dir));
@@ -1982,6 +2129,7 @@ function writeClaudeCustomAgents(opts) {
1982
2129
  for (const agent of claudeCustomAgents()) {
1983
2130
  writeFile(join(home, "agents", agent.filename), agent.content, opts);
1984
2131
  }
2132
+ writeClaudeCommands(opts);
1985
2133
  for (const agent of legacyClaudeCustomAgents()) {
1986
2134
  const legacyPath = join(home, "agents", agent.filename);
1987
2135
  logVerbose(
@@ -1992,6 +2140,13 @@ function writeClaudeCustomAgents(opts) {
1992
2140
  removeLegacyClaudeCommands(home, opts);
1993
2141
  }
1994
2142
 
2143
+ function writeClaudeCommands(opts) {
2144
+ const home = claudeHome();
2145
+ for (const command of claudeCommands()) {
2146
+ writeFile(join(home, "commands", command.filename), command.content, opts);
2147
+ }
2148
+ }
2149
+
1995
2150
  function removeLegacyClaudeCommands(home, opts) {
1996
2151
  for (const command of legacyClaudeCommands()) {
1997
2152
  const commandPath = join(home, "commands", command.filename);
@@ -2277,12 +2432,15 @@ function installClaude(opts) {
2277
2432
  throw new Error(message);
2278
2433
  }
2279
2434
  writeClaudeCustomAgents(opts);
2435
+ removeClaudeMcp(opts);
2280
2436
  if (opts.server === "hosted") {
2281
2437
  run(
2282
2438
  "claude",
2283
2439
  [
2284
2440
  "mcp",
2285
2441
  "add",
2442
+ "--scope",
2443
+ "user",
2286
2444
  "--transport",
2287
2445
  "http",
2288
2446
  "sellable",
@@ -2294,14 +2452,20 @@ function installClaude(opts) {
2294
2452
  return true;
2295
2453
  }
2296
2454
  const [command, args] = mcpCommand(opts);
2297
- run("claude", ["mcp", "remove", "sellable"], {
2298
- ...opts,
2299
- dryRun: opts.dryRun,
2300
- allowFail: true,
2301
- });
2302
2455
  run(
2303
2456
  "claude",
2304
- ["mcp", "add", "--transport", "stdio", "sellable", "--", command, ...args],
2457
+ [
2458
+ "mcp",
2459
+ "add",
2460
+ "--scope",
2461
+ "user",
2462
+ "--transport",
2463
+ "stdio",
2464
+ "sellable",
2465
+ "--",
2466
+ command,
2467
+ ...args,
2468
+ ],
2305
2469
  opts
2306
2470
  );
2307
2471
  // Patch ~/.claude.json to add `alwaysLoad: true` so Claude Code v2.1.121+
@@ -2312,6 +2476,63 @@ function installClaude(opts) {
2312
2476
  return true;
2313
2477
  }
2314
2478
 
2479
+ function removeClaudeMcp(opts) {
2480
+ for (const scope of ["local", "project", "user"]) {
2481
+ run("claude", ["mcp", "remove", "--scope", scope, "sellable"], {
2482
+ ...opts,
2483
+ dryRun: opts.dryRun,
2484
+ allowFail: true,
2485
+ });
2486
+ }
2487
+ }
2488
+
2489
+ function canonicalClaudeMcpServer(opts) {
2490
+ if (opts.server === "hosted") {
2491
+ return {
2492
+ type: "http",
2493
+ url: withHostedWatchModeDriver(opts.hostedUrl, "claude"),
2494
+ alwaysLoad: true,
2495
+ };
2496
+ }
2497
+
2498
+ const [command, args] = mcpCommand(opts);
2499
+ return {
2500
+ type: "stdio",
2501
+ command,
2502
+ args,
2503
+ env: { SELLABLE_WATCH_MODE_DRIVER: "claude" },
2504
+ alwaysLoad: true,
2505
+ };
2506
+ }
2507
+
2508
+ function collectClaudeSellableServers(config) {
2509
+ const servers = [];
2510
+ if (config?.mcpServers?.sellable) {
2511
+ servers.push(config.mcpServers.sellable);
2512
+ }
2513
+ for (const projectPath of Object.keys(config?.projects || {})) {
2514
+ const server = config.projects[projectPath]?.mcpServers?.sellable;
2515
+ if (server) {
2516
+ servers.push(server);
2517
+ }
2518
+ }
2519
+ return servers;
2520
+ }
2521
+
2522
+ function claudeMcpServerMatches(server, canonical) {
2523
+ if (!server || typeof server !== "object") return false;
2524
+ if (server.alwaysLoad !== true) return false;
2525
+ if (canonical.type === "http") {
2526
+ return server.type === canonical.type && server.url === canonical.url;
2527
+ }
2528
+ return (
2529
+ server.type === canonical.type &&
2530
+ server.command === canonical.command &&
2531
+ JSON.stringify(server.args || []) === JSON.stringify(canonical.args) &&
2532
+ server.env?.SELLABLE_WATCH_MODE_DRIVER === "claude"
2533
+ );
2534
+ }
2535
+
2315
2536
  function patchClaudeAlwaysLoad(opts) {
2316
2537
  if (opts.dryRun) {
2317
2538
  logVerbose(
@@ -2329,18 +2550,44 @@ function patchClaudeAlwaysLoad(opts) {
2329
2550
  try {
2330
2551
  const raw = readFileSync(claudeJsonPath, "utf8");
2331
2552
  const config = JSON.parse(raw);
2553
+ const canonical = canonicalClaudeMcpServer(opts);
2332
2554
  let touched = false;
2333
2555
  const patchSellableServer = (server) => {
2334
2556
  if (!server || typeof server !== "object") return;
2335
- if (server.alwaysLoad !== true) {
2336
- server.alwaysLoad = true;
2337
- touched = true;
2338
- }
2339
- const isHttpServer =
2340
- typeof server.url === "string" ||
2341
- server.type === "http" ||
2342
- server.transport === "http";
2343
- if (!isHttpServer) {
2557
+ if (opts.server === "hosted") {
2558
+ for (const staleKey of ["command", "args", "env"]) {
2559
+ if (staleKey in server) {
2560
+ delete server[staleKey];
2561
+ touched = true;
2562
+ }
2563
+ }
2564
+ if (server.type !== canonical.type) {
2565
+ server.type = canonical.type;
2566
+ touched = true;
2567
+ }
2568
+ if (server.url !== canonical.url) {
2569
+ server.url = canonical.url;
2570
+ touched = true;
2571
+ }
2572
+ } else {
2573
+ for (const staleKey of ["url"]) {
2574
+ if (staleKey in server) {
2575
+ delete server[staleKey];
2576
+ touched = true;
2577
+ }
2578
+ }
2579
+ if (server.type !== canonical.type) {
2580
+ server.type = canonical.type;
2581
+ touched = true;
2582
+ }
2583
+ if (server.command !== canonical.command) {
2584
+ server.command = canonical.command;
2585
+ touched = true;
2586
+ }
2587
+ if (JSON.stringify(server.args || []) !== JSON.stringify(canonical.args)) {
2588
+ server.args = [...canonical.args];
2589
+ touched = true;
2590
+ }
2344
2591
  const existingDriver = server.env?.SELLABLE_WATCH_MODE_DRIVER;
2345
2592
  server.env = {
2346
2593
  ...(server.env && typeof server.env === "object" ? server.env : {}),
@@ -2350,12 +2597,16 @@ function patchClaudeAlwaysLoad(opts) {
2350
2597
  touched = true;
2351
2598
  }
2352
2599
  }
2600
+ if (server.alwaysLoad !== true) {
2601
+ server.alwaysLoad = true;
2602
+ touched = true;
2603
+ }
2353
2604
  };
2354
- // Top-level mcpServers.sellable (older claude versions)
2605
+ // Top-level mcpServers.sellable (user scope)
2355
2606
  if (config.mcpServers?.sellable) {
2356
2607
  patchSellableServer(config.mcpServers.sellable);
2357
2608
  }
2358
- // Per-project mcpServers.sellable (current claude default)
2609
+ // Per-project mcpServers.sellable (old local/project installs)
2359
2610
  for (const projectPath of Object.keys(config.projects || {})) {
2360
2611
  const projServers = config.projects[projectPath]?.mcpServers;
2361
2612
  if (projServers?.sellable) {
@@ -2469,6 +2720,34 @@ async function verify(opts) {
2469
2720
  : "Claude custom agent MCP tool allowlists missing"
2470
2721
  )
2471
2722
  );
2723
+ const claudeCommandPaths = claudeCommands().map((command) =>
2724
+ join(claudeHome(), "commands", command.filename)
2725
+ );
2726
+ const hasClaudeCommands = claudeCommandPaths.every((commandPath) =>
2727
+ existsSync(commandPath)
2728
+ );
2729
+ checks.push(
2730
+ warningCheck(
2731
+ hasClaudeCommands,
2732
+ hasClaudeCommands
2733
+ ? "Claude slash commands present"
2734
+ : "Claude slash commands missing"
2735
+ )
2736
+ );
2737
+ const claudeCommandsHaveCurrentNames = claudeCommands().every((command) => {
2738
+ const commandPath = join(claudeHome(), "commands", command.filename);
2739
+ if (!existsSync(commandPath)) return false;
2740
+ const content = readFileSync(commandPath, "utf8");
2741
+ return content.includes(`name: sellable:${command.name}`);
2742
+ });
2743
+ checks.push(
2744
+ warningCheck(
2745
+ claudeCommandsHaveCurrentNames,
2746
+ claudeCommandsHaveCurrentNames
2747
+ ? "Claude slash command names current"
2748
+ : "Claude slash command names stale"
2749
+ )
2750
+ );
2472
2751
  const legacyClaudePaths = [
2473
2752
  ...legacyClaudeCustomAgents().map((agent) =>
2474
2753
  join(claudeHome(), "agents", agent.filename)
@@ -2504,6 +2783,25 @@ async function verify(opts) {
2504
2783
  : "Claude watch mode driver missing"
2505
2784
  )
2506
2785
  );
2786
+ let claudeMcpEntriesCanonical = false;
2787
+ try {
2788
+ const config = claudeConfigContent ? JSON.parse(claudeConfigContent) : {};
2789
+ const servers = collectClaudeSellableServers(config);
2790
+ const canonical = canonicalClaudeMcpServer(opts);
2791
+ claudeMcpEntriesCanonical =
2792
+ servers.length > 0 &&
2793
+ servers.every((server) => claudeMcpServerMatches(server, canonical));
2794
+ } catch {
2795
+ claudeMcpEntriesCanonical = false;
2796
+ }
2797
+ checks.push(
2798
+ warningCheck(
2799
+ claudeMcpEntriesCanonical,
2800
+ claudeMcpEntriesCanonical
2801
+ ? "Claude Sellable MCP entries canonical"
2802
+ : "Claude Sellable MCP entries stale"
2803
+ )
2804
+ );
2507
2805
  }
2508
2806
  if (opts.host === "codex" || opts.host === "all") {
2509
2807
  const hasCodexCli = commandExists("codex");
@@ -2886,7 +3184,9 @@ function printNextSteps(installedHosts, authReused) {
2886
3184
  console.log(` ${C.green}✓${C.reset} Codex custom agents installed`);
2887
3185
  }
2888
3186
  if (hasClaude) {
2889
- console.log(` ${C.green}✓${C.reset} Claude custom agents installed`);
3187
+ console.log(
3188
+ ` ${C.green}✓${C.reset} Claude slash commands and custom agents installed`
3189
+ );
2890
3190
  }
2891
3191
  if (authReused) {
2892
3192
  console.log(
@@ -3200,6 +3500,45 @@ async function main() {
3200
3500
  console.log(` Codex: $sellable:create-post`);
3201
3501
  process.exit(0);
3202
3502
  }
3503
+ if (rawArgs[0] === "prefs") {
3504
+ const dryRun = rawArgs.includes("--dry-run");
3505
+ const args = rawArgs.filter((arg) => arg !== "--dry-run");
3506
+ if (
3507
+ args[1] !== "set" ||
3508
+ args[2] !== "prompt-sharing" ||
3509
+ (args[3] !== "true" && args[3] !== "false") ||
3510
+ args.length !== 4
3511
+ ) {
3512
+ console.error(
3513
+ "Usage: sellable prefs set prompt-sharing true|false [--dry-run]"
3514
+ );
3515
+ process.exit(2);
3516
+ }
3517
+ const envPath = writePromptSharingPreference(args[3], { dryRun });
3518
+ console.log(
3519
+ `✓ Sellable prompt sharing preference set to ${args[3]} at ${envPath}`
3520
+ );
3521
+ process.exit(0);
3522
+ }
3523
+ if (rawArgs[0] === "setup") {
3524
+ const dryRun = rawArgs.includes("--dry-run");
3525
+ const args = rawArgs.filter((arg) => arg !== "--dry-run");
3526
+ if (args[1] !== "claude-permissions") {
3527
+ console.error(
3528
+ `Unknown setup subcommand: ${args[1] ?? "(none)"}\nKnown: claude-permissions`
3529
+ );
3530
+ process.exit(2);
3531
+ }
3532
+ if (!args.includes("--allow-common-setup")) {
3533
+ console.error(
3534
+ "Usage: sellable setup claude-permissions --allow-common-setup [--dry-run]"
3535
+ );
3536
+ process.exit(2);
3537
+ }
3538
+ const settingsPath = allowClaudeCommonSetup({ dryRun });
3539
+ console.log(`✓ Claude Sellable permissions updated at ${settingsPath}`);
3540
+ process.exit(0);
3541
+ }
3203
3542
  if (rawArgs[0] === "uninstall") {
3204
3543
  runUninstall();
3205
3544
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.208",
3
+ "version": "0.1.209",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {