@sellable/install 0.1.326 → 0.1.328

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.
@@ -48,7 +48,7 @@ function getInstallVersion() {
48
48
  }
49
49
  }
50
50
 
51
- const CODEX_PLUGIN_VERSION = "0.1.56";
51
+ const CODEX_PLUGIN_VERSION = "0.1.58";
52
52
  const CODEX_PLUGIN_COMPAT_VERSIONS = [
53
53
  "0.1.8",
54
54
  "0.1.9",
@@ -2064,7 +2064,41 @@ installPackageVersion: ${getInstallVersion()}
2064
2064
  codexPluginVersion: ${CODEX_PLUGIN_VERSION}
2065
2065
  <!-- REFILL_CONTRACT_GENERATED:END -->`;
2066
2066
 
2067
- function refillSendsSkillMd(host = "shared") {
2067
+ function refillReleaseIdentityBlock(host, opts = {}) {
2068
+ const match = /^@sellable\/mcp@(.+)$/.exec(opts.mcpPackage || "");
2069
+ const mcpVersion = match?.[1] || String(opts.mcpPackage || "unknown");
2070
+ const pluginVersion = host === "codex" ? CODEX_PLUGIN_VERSION : "not-applicable";
2071
+ const cacheIdentity =
2072
+ host === "codex"
2073
+ ? `codex-plugin:${pluginVersion}|mcp:${mcpVersion}`
2074
+ : `${host}-host|mcp:${mcpVersion}`;
2075
+ return `## Installed release identity
2076
+
2077
+ expectedMcpPackageVersion: ${mcpVersion}
2078
+ expectedInstallPackageVersion: ${getInstallVersion()}
2079
+ expectedCodexPluginVersion: ${pluginVersion}
2080
+ expectedPluginCacheIdentity: ${cacheIdentity}
2081
+
2082
+ Immediately after \`get_auth_status\`, require
2083
+ \`update.mcp.currentVersion\` to equal \`${mcpVersion}\`. If it is missing or
2084
+ different, stop with \`installed_mcp_identity_mismatch\` before any product mutation.
2085
+ Do not call \`get_refill_target_plan\`, \`refill_sends\`, or another mutating
2086
+ workflow tool. Explain that Codex is still using a stale Sellable MCP runtime,
2087
+ then fully quit and reopen Codex Desktop and start a new thread.`;
2088
+ }
2089
+
2090
+ function stampRefillReleaseIdentity(markdown, host, opts) {
2091
+ const marker = markdown.includes("## Bootstrap and execute")
2092
+ ? "## Bootstrap and execute"
2093
+ : "## Bootstrap";
2094
+ if (!markdown.includes(marker)) return markdown;
2095
+ return markdown.replace(
2096
+ marker,
2097
+ `${refillReleaseIdentityBlock(host, opts)}\n\n${marker}`
2098
+ );
2099
+ }
2100
+
2101
+ function refillSendsSkillMd(host = "shared", opts = {}) {
2068
2102
  try {
2069
2103
  const here = dirname(fileURLToPath(import.meta.url));
2070
2104
  const templatePath = join(
@@ -2078,9 +2112,13 @@ function refillSendsSkillMd(host = "shared") {
2078
2112
  existsSync(templatePath)
2079
2113
  ) {
2080
2114
  return stampInstalledHost(
2081
- addOrReplaceAllowedTools(
2082
- readFileSync(templatePath, "utf8"),
2083
- REFILL_SENDS_ALLOWED_TOOLS
2115
+ stampRefillReleaseIdentity(
2116
+ addOrReplaceAllowedTools(
2117
+ readFileSync(templatePath, "utf8"),
2118
+ REFILL_SENDS_ALLOWED_TOOLS
2119
+ ),
2120
+ host,
2121
+ opts
2084
2122
  ),
2085
2123
  host,
2086
2124
  "refill-sends"
@@ -2089,7 +2127,7 @@ function refillSendsSkillMd(host = "shared") {
2089
2127
  } catch {
2090
2128
  // fall through to hardcoded fallback below
2091
2129
  }
2092
- return stampInstalledHost(`---
2130
+ return stampInstalledHost(stampRefillReleaseIdentity(`---
2093
2131
  name: refill-sends
2094
2132
  description: Refill Sellable sender sends through the approval-gated refill workflow.
2095
2133
  visibility: public
@@ -2156,7 +2194,7 @@ Desktop, then start a new thread.
2156
2194
  If exact subskill lookup fails, use
2157
2195
  \`mcp__sellable__search_subskill_prompts({ query: "refill-sends", includePublic: true, includeInternal: true })\`,
2158
2196
  then retry \`get_subskill_prompt\`.
2159
- `, host, "refill-sends");
2197
+ `, host, opts), host, "refill-sends");
2160
2198
  }
2161
2199
 
2162
2200
  function refreshSenderEngagementSkillMd(host = "shared") {
@@ -2668,7 +2706,7 @@ will change or after they approve the settings update.
2668
2706
  `;
2669
2707
  }
2670
2708
 
2671
- function codexPluginSkills() {
2709
+ function codexPluginSkills(opts = {}) {
2672
2710
  return [
2673
2711
  {
2674
2712
  dir: "sellable-create-campaign",
@@ -2717,7 +2755,7 @@ function codexPluginSkills() {
2717
2755
  dir: "sellable-refill-sends",
2718
2756
  displayName: "Sellable Refill Sends",
2719
2757
  description: "Refill sender sends with approval-gated campaign selection",
2720
- skillMd: refillSendsSkillMd("codex"),
2758
+ skillMd: refillSendsSkillMd("codex", opts),
2721
2759
  },
2722
2760
  ];
2723
2761
  }
@@ -2908,7 +2946,7 @@ then retry \`get_subskill_prompt\`.
2908
2946
  );
2909
2947
  }
2910
2948
 
2911
- function hermesSkillDefinitions() {
2949
+ function hermesSkillDefinitions(opts = {}) {
2912
2950
  return [
2913
2951
  {
2914
2952
  commandName: "create-campaign",
@@ -2965,7 +3003,7 @@ function hermesSkillDefinitions() {
2965
3003
  commandName: "refill-sends",
2966
3004
  dir: hermesCommandName("refill-sends"),
2967
3005
  skillMd: normalizeHermesSkillMarkdown(
2968
- refillSendsSkillMd("hermes"),
3006
+ refillSendsSkillMd("hermes", opts),
2969
3007
  "refill-sends"
2970
3008
  ),
2971
3009
  },
@@ -3158,7 +3196,7 @@ ${stripFrontmatter(command.skillMd)}
3158
3196
  `;
3159
3197
  }
3160
3198
 
3161
- function claudeCommands() {
3199
+ function claudeCommands(opts = {}) {
3162
3200
  return [
3163
3201
  {
3164
3202
  name: "create-campaign",
@@ -3214,7 +3252,7 @@ function claudeCommands() {
3214
3252
  description:
3215
3253
  "Refill Sellable sender sends through the approval-gated refill workflow.",
3216
3254
  argumentHint: "[--yolo] [--sender name-or-id ...]",
3217
- skillMd: refillSendsSkillMd("claude"),
3255
+ skillMd: refillSendsSkillMd("claude", opts),
3218
3256
  allowedTools: ["AskUserQuestion", ...REFILL_SENDS_ALLOWED_TOOLS],
3219
3257
  },
3220
3258
  {
@@ -3239,7 +3277,7 @@ function claudeCommands() {
3239
3277
  }
3240
3278
 
3241
3279
  function writeCodexPluginSkills(pluginRoot, opts) {
3242
- const skills = codexPluginSkills();
3280
+ const skills = codexPluginSkills(opts);
3243
3281
  const currentSkillDirs = new Set(skills.map((skill) => skill.dir));
3244
3282
  const skillsRoot = join(pluginRoot, "skills");
3245
3283
 
@@ -3303,7 +3341,7 @@ function writeClaudeCustomAgents(opts) {
3303
3341
 
3304
3342
  function writeClaudeCommands(opts) {
3305
3343
  const home = claudeHome();
3306
- for (const command of claudeCommands()) {
3344
+ for (const command of claudeCommands(opts)) {
3307
3345
  writeFile(join(home, "commands", command.filename), command.content, opts);
3308
3346
  }
3309
3347
  }
@@ -3815,7 +3853,7 @@ function writeHermesMcpServer(opts) {
3815
3853
 
3816
3854
  function installHermesSkills(opts) {
3817
3855
  const root = hermesSkillsRoot(opts);
3818
- for (const skill of hermesSkillDefinitions()) {
3856
+ for (const skill of hermesSkillDefinitions(opts)) {
3819
3857
  const skillRoot = join(root, skill.dir);
3820
3858
  writeFile(join(skillRoot, "SKILL.md"), skill.skillMd, opts);
3821
3859
  if (skill.soulMd) {
@@ -86,6 +86,54 @@ function treeHash(root) {
86
86
  return canonicalDigest(treeRecords(root));
87
87
  }
88
88
 
89
+ function quoteToml(value) {
90
+ return JSON.stringify(String(value));
91
+ }
92
+
93
+ function removeTomlTable(content, header) {
94
+ const lines = content.split("\n");
95
+ const target = `[${header}]`;
96
+ const kept = [];
97
+ let skipping = false;
98
+ for (const line of lines) {
99
+ const trimmed = line.trim();
100
+ if (/^\[[^\]]+\]$/.test(trimmed)) {
101
+ skipping = trimmed === target;
102
+ if (skipping) continue;
103
+ }
104
+ if (!skipping) kept.push(line);
105
+ }
106
+ return kept.join("\n");
107
+ }
108
+
109
+ function candidateCodexConfig(content, candidatePath) {
110
+ const mcpPath = join(candidatePath, ".mcp.json");
111
+ if (!existsSync(mcpPath)) throw new Error("codex_candidate_mcp_manifest_missing");
112
+ const manifest = JSON.parse(readFileSync(mcpPath, "utf8"));
113
+ const server = manifest?.mcpServers?.sellable;
114
+ if (
115
+ server?.type !== "stdio" ||
116
+ typeof server.command !== "string" ||
117
+ !Array.isArray(server.args) ||
118
+ server.args.some((value) => typeof value !== "string") ||
119
+ !server.env ||
120
+ typeof server.env !== "object" ||
121
+ Object.values(server.env).some((value) => typeof value !== "string")
122
+ ) {
123
+ throw new Error("codex_candidate_mcp_manifest_invalid");
124
+ }
125
+ let next = removeTomlTable(content, "mcp_servers.sellable.env");
126
+ next = removeTomlTable(next, "mcp_servers.sellable").trimEnd();
127
+ const args = `[${server.args.map(quoteToml).join(", ")}]`;
128
+ const env = Object.entries(server.env)
129
+ .sort(([left], [right]) => left.localeCompare(right))
130
+ .map(([key, value]) => `${key} = ${quoteToml(value)}`)
131
+ .join("\n");
132
+ return `${next}\n\n[mcp_servers.sellable]\ncommand = ${quoteToml(
133
+ server.command
134
+ )}\nargs = ${args}\n\n[mcp_servers.sellable.env]\n${env}\n`;
135
+ }
136
+
89
137
  function pluginVersion(pluginRoot) {
90
138
  const manifestPath = join(pluginRoot, ".codex-plugin", "plugin.json");
91
139
  if (!existsSync(manifestPath)) throw new Error("codex_selection_plugin_manifest_missing");
@@ -240,6 +288,20 @@ function atomicReplaceTree(source, target) {
240
288
  }
241
289
  }
242
290
 
291
+ function atomicReplaceFile(content, target) {
292
+ const parent = dirname(target);
293
+ mkdirSync(parent, { recursive: true });
294
+ const staging = join(parent, `.${basename(target)}.phase98-next-${process.pid}`);
295
+ const mode = existsSync(target) ? lstatSync(target).mode & 0o777 : 0o600;
296
+ writeFileSync(staging, content, { mode });
297
+ try {
298
+ renameSync(staging, target);
299
+ } catch (error) {
300
+ rmSync(staging, { force: true });
301
+ throw error;
302
+ }
303
+ }
304
+
243
305
  export function activateCodexCandidateSelection(input) {
244
306
  const {
245
307
  paths,
@@ -271,10 +333,30 @@ export function activateCodexCandidateSelection(input) {
271
333
  if (!existsSync(candidatePath) || treeHash(candidatePath) !== expectedCandidateHash) {
272
334
  throw new Error("codex_selection_candidate_cache_mismatch");
273
335
  }
274
- atomicReplaceTree(candidatePath, paths.pluginRoot);
275
- if (pluginVersion(paths.pluginRoot) !== candidateVersion || treeHash(paths.pluginRoot) !== expectedCandidateHash) {
276
- throw new Error("codex_selection_candidate_activation_mismatch");
336
+ const candidateConfig = candidateCodexConfig(
337
+ readFileSync(paths.configPath, "utf8"),
338
+ candidatePath
339
+ );
340
+ const configBackup = snapshot.items.find((item) => item.name === "config");
341
+ const pluginBackup = snapshot.items.find((item) => item.name === "pluginRoot");
342
+ if (!configBackup || !pluginBackup) {
343
+ throw new Error("codex_selection_snapshot_restore_items_missing");
344
+ }
345
+ try {
346
+ atomicReplaceFile(candidateConfig, paths.configPath);
347
+ atomicReplaceTree(candidatePath, paths.pluginRoot);
348
+ if (
349
+ pluginVersion(paths.pluginRoot) !== candidateVersion ||
350
+ treeHash(paths.pluginRoot) !== expectedCandidateHash
351
+ ) {
352
+ throw new Error("codex_selection_candidate_activation_mismatch");
353
+ }
354
+ } catch (error) {
355
+ copyExact(configBackup.backupPath, configBackup.sourcePath);
356
+ copyExact(pluginBackup.backupPath, pluginBackup.sourcePath);
357
+ throw error;
277
358
  }
359
+ const candidateConfigHash = treeHash(paths.configPath);
278
360
  const activation = {
279
361
  schemaVersion: CODEX_PLUGIN_SELECTION_SCHEMA_VERSION,
280
362
  approvalId,
@@ -285,7 +367,8 @@ export function activateCodexCandidateSelection(input) {
285
367
  stableVersion: snapshot.pluginVersion,
286
368
  candidateVersion,
287
369
  candidateHash: expectedCandidateHash,
288
- changedPaths: [paths.pluginRoot],
370
+ candidateConfigHash,
371
+ changedPaths: [paths.pluginRoot, paths.configPath],
289
372
  };
290
373
  const activationPath = join(dirname(snapshotManifestPath), "activation.json");
291
374
  writeFileSync(activationPath, `${JSON.stringify(activation, null, 2)}\n`, { mode: 0o600 });
@@ -310,6 +393,12 @@ export function restoreCodexPluginSelection(input) {
310
393
  activation.snapshotHash !== expectedSnapshotHash ||
311
394
  activation.candidateHash !== treeHash(paths.pluginRoot)
312
395
  ) throw new Error("codex_selection_active_candidate_drift");
396
+ if (
397
+ !activation.candidateConfigHash ||
398
+ activation.candidateConfigHash !== treeHash(paths.configPath)
399
+ ) {
400
+ throw new Error("codex_selection_active_config_drift");
401
+ }
313
402
  if (!recoveryReason) {
314
403
  if (!taskEvidence?.taskId || taskEvidence.resolvedCandidateHash !== activation.candidateHash) {
315
404
  throw new Error("codex_selection_task_identity_mismatch");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.326",
3
+ "version": "0.1.328",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {