@sellable/install 0.1.212 → 0.1.214
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 +6 -3
- package/bin/sellable-install.mjs +197 -15
- package/lib/runtime-verify.mjs +92 -1
- package/package.json +1 -1
- package/skill-templates/create-campaign.md +8 -0
package/README.md
CHANGED
|
@@ -31,9 +31,9 @@ agent command for launching a campaign:
|
|
|
31
31
|
sellable create
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Campaign creation, foundation memory, content capture/ideation,
|
|
35
|
-
|
|
36
|
-
approval flows are available.
|
|
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.
|
|
37
37
|
|
|
38
38
|
Install is auth-free by default. If you do not pass a token, the agent handles
|
|
39
39
|
Sellable sign-in on the first campaign run with a magic-link handoff.
|
|
@@ -97,17 +97,20 @@ Use the same public entrypoints in both hosts:
|
|
|
97
97
|
- Claude Code: `/sellable:foundation`
|
|
98
98
|
- Claude Code: `/sellable:content`
|
|
99
99
|
- Claude Code: `/sellable:create-post`
|
|
100
|
+
- Claude Code: `/sellable:inbox`
|
|
100
101
|
- Codex: `$sellable:create-campaign`
|
|
101
102
|
- Codex: `$sellable:create-ab-test`
|
|
102
103
|
- Codex: `$sellable:foundation`
|
|
103
104
|
- Codex: `$sellable:content`
|
|
104
105
|
- Codex: `$sellable:create-post`
|
|
106
|
+
- Codex: `$sellable:inbox`
|
|
105
107
|
- Codex Desktop plugin: `sellable@sellable`
|
|
106
108
|
- Codex visible skill: `Sellable Create Campaign`
|
|
107
109
|
- Codex visible skill: `Sellable Create A/B Test`
|
|
108
110
|
- Codex visible skill: `Sellable Foundation`
|
|
109
111
|
- Codex visible skill: `Sellable Content`
|
|
110
112
|
- Codex visible skill: `Sellable Create Post`
|
|
113
|
+
- Codex visible skill: `Sellable Inbox`
|
|
111
114
|
- Internal MCP workflow prompt: `create-campaign-v2`
|
|
112
115
|
- Internal/backward-compatible memory prompt: `interview`
|
|
113
116
|
|
package/bin/sellable-install.mjs
CHANGED
|
@@ -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,
|
|
185
|
+
/sellable:create-campaign, /sellable:foundation, /sellable:content, /sellable:create-post, or /sellable:inbox 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,6 +211,7 @@ 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" },
|
|
214
215
|
]);
|
|
215
216
|
console.log("");
|
|
216
217
|
console.log("");
|
|
@@ -220,6 +221,7 @@ function printCreateCommandHint() {
|
|
|
220
221
|
{ label: "Foundation", command: "$sellable:foundation" },
|
|
221
222
|
{ label: "Content", command: "$sellable:content" },
|
|
222
223
|
{ label: "Post", command: "$sellable:create-post" },
|
|
224
|
+
{ label: "Inbox", command: "$sellable:inbox" },
|
|
223
225
|
]);
|
|
224
226
|
console.log("");
|
|
225
227
|
console.log(` ${"─".repeat(63)}`);
|
|
@@ -330,10 +332,6 @@ function isWindowsPlatform(platform = process.platform) {
|
|
|
330
332
|
return platform === "win32";
|
|
331
333
|
}
|
|
332
334
|
|
|
333
|
-
function packageRunnerCommand(platform = process.platform) {
|
|
334
|
-
return isWindowsPlatform(platform) ? "npx.cmd" : "npx";
|
|
335
|
-
}
|
|
336
|
-
|
|
337
335
|
function packageManagerCommand(platform = process.platform) {
|
|
338
336
|
return isWindowsPlatform(platform) ? "npm.cmd" : "npm";
|
|
339
337
|
}
|
|
@@ -551,6 +549,14 @@ function upsertTomlTable(content, tableName, block) {
|
|
|
551
549
|
return `${content.trimEnd()}\n\n${normalizedBlock}\n`;
|
|
552
550
|
}
|
|
553
551
|
|
|
552
|
+
function readTomlTable(content, tableName) {
|
|
553
|
+
const tablePattern = new RegExp(
|
|
554
|
+
`(^|\\n)\\[${escapeRegExp(tableName)}\\]\\n([\\s\\S]*?)(?=\\n\\[[^\\n]+\\]|$)`
|
|
555
|
+
);
|
|
556
|
+
const match = content.match(tablePattern);
|
|
557
|
+
return match ? match[2] : "";
|
|
558
|
+
}
|
|
559
|
+
|
|
554
560
|
function upsertTomlBoolean(content, tableName, key, value) {
|
|
555
561
|
const line = `${key} = ${value ? "true" : "false"}`;
|
|
556
562
|
const tablePattern = new RegExp(
|
|
@@ -678,12 +684,13 @@ function codexPluginMcp(opts) {
|
|
|
678
684
|
};
|
|
679
685
|
}
|
|
680
686
|
|
|
687
|
+
const [command, args] = mcpCommand(opts);
|
|
681
688
|
return {
|
|
682
689
|
mcpServers: {
|
|
683
690
|
sellable: {
|
|
684
691
|
type: "stdio",
|
|
685
|
-
command
|
|
686
|
-
args
|
|
692
|
+
command,
|
|
693
|
+
args,
|
|
687
694
|
env: {
|
|
688
695
|
SELLABLE_WATCH_MODE_DRIVER: "codex",
|
|
689
696
|
},
|
|
@@ -1748,6 +1755,67 @@ then retry \`get_subskill_prompt\`.
|
|
|
1748
1755
|
`, host, "create-post");
|
|
1749
1756
|
}
|
|
1750
1757
|
|
|
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
|
+
|
|
1751
1819
|
function createCampaignSoulMd() {
|
|
1752
1820
|
return `# Sellable Campaign GTM Engineer Soul
|
|
1753
1821
|
|
|
@@ -1969,6 +2037,12 @@ function codexPluginSkills() {
|
|
|
1969
2037
|
description: "Capture ideas and draft LinkedIn posts in your voice",
|
|
1970
2038
|
skillMd: createPostSkillMd(),
|
|
1971
2039
|
},
|
|
2040
|
+
{
|
|
2041
|
+
dir: "sellable-inbox",
|
|
2042
|
+
displayName: "Sellable Inbox",
|
|
2043
|
+
description: "Search and manage approval-gated LinkedIn inbox replies",
|
|
2044
|
+
skillMd: inboxSkillMd(),
|
|
2045
|
+
},
|
|
1972
2046
|
];
|
|
1973
2047
|
}
|
|
1974
2048
|
|
|
@@ -2194,6 +2268,23 @@ function claudeCommands() {
|
|
|
2194
2268
|
argumentHint: "[post idea or source material]",
|
|
2195
2269
|
skillMd: createPostSkillMd(),
|
|
2196
2270
|
},
|
|
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
|
+
},
|
|
2197
2288
|
].map((command) => ({
|
|
2198
2289
|
...command,
|
|
2199
2290
|
content: claudeCommandMd(command),
|
|
@@ -2386,6 +2477,7 @@ enabled = false`
|
|
|
2386
2477
|
"default_mode_request_user_input",
|
|
2387
2478
|
true
|
|
2388
2479
|
);
|
|
2480
|
+
content = upsertCodexMcpServerConfig(content, opts);
|
|
2389
2481
|
content = upsertTomlTable(
|
|
2390
2482
|
content,
|
|
2391
2483
|
"agents",
|
|
@@ -2544,6 +2636,68 @@ function codexMcpAddArgs(opts) {
|
|
|
2544
2636
|
];
|
|
2545
2637
|
}
|
|
2546
2638
|
|
|
2639
|
+
function upsertCodexMcpServerConfig(content, opts) {
|
|
2640
|
+
if (opts.server === "hosted") {
|
|
2641
|
+
content = upsertTomlTable(
|
|
2642
|
+
content,
|
|
2643
|
+
"mcp_servers.sellable",
|
|
2644
|
+
`[mcp_servers.sellable]
|
|
2645
|
+
url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}`
|
|
2646
|
+
);
|
|
2647
|
+
content = removeTomlSection(content, "mcp_servers.sellable.env");
|
|
2648
|
+
return content;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
const [command, args] = mcpCommand(opts);
|
|
2652
|
+
content = upsertTomlTable(
|
|
2653
|
+
content,
|
|
2654
|
+
"mcp_servers.sellable",
|
|
2655
|
+
`[mcp_servers.sellable]
|
|
2656
|
+
command = ${quoteToml(command)}
|
|
2657
|
+
args = ${tomlArray(args)}`
|
|
2658
|
+
);
|
|
2659
|
+
content = upsertTomlTable(
|
|
2660
|
+
content,
|
|
2661
|
+
"mcp_servers.sellable.env",
|
|
2662
|
+
`[mcp_servers.sellable.env]
|
|
2663
|
+
SELLABLE_WATCH_MODE_DRIVER = "codex"`
|
|
2664
|
+
);
|
|
2665
|
+
return content;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
function codexMcpServerMatches(content, opts) {
|
|
2669
|
+
const server = readTomlTable(content, "mcp_servers.sellable");
|
|
2670
|
+
if (!server) return false;
|
|
2671
|
+
if (opts.server === "hosted") {
|
|
2672
|
+
return (
|
|
2673
|
+
server.includes(
|
|
2674
|
+
`url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}`
|
|
2675
|
+
) &&
|
|
2676
|
+
!/\bcommand\s*=/.test(server) &&
|
|
2677
|
+
!/\bargs\s*=/.test(server)
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
const [command, args] = mcpCommand(opts);
|
|
2682
|
+
const env = readTomlTable(content, "mcp_servers.sellable.env");
|
|
2683
|
+
return (
|
|
2684
|
+
server.includes(`command = ${quoteToml(command)}`) &&
|
|
2685
|
+
server.includes(`args = ${tomlArray(args)}`) &&
|
|
2686
|
+
env.includes('SELLABLE_WATCH_MODE_DRIVER = "codex"')
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
function codexPluginMcpServerMatches(content, opts) {
|
|
2691
|
+
if (!content) return false;
|
|
2692
|
+
try {
|
|
2693
|
+
const actual = JSON.parse(content)?.mcpServers?.sellable;
|
|
2694
|
+
const expected = codexPluginMcp(opts).mcpServers.sellable;
|
|
2695
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
2696
|
+
} catch {
|
|
2697
|
+
return false;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2547
2701
|
function installClaude(opts) {
|
|
2548
2702
|
if (!commandExists("claude")) {
|
|
2549
2703
|
const message =
|
|
@@ -3027,6 +3181,30 @@ async function verify(opts) {
|
|
|
3027
3181
|
: "Codex Desktop plugin watch mode driver missing"
|
|
3028
3182
|
)
|
|
3029
3183
|
);
|
|
3184
|
+
const codexMcpEntriesCanonical = codexMcpServerMatches(
|
|
3185
|
+
configContent,
|
|
3186
|
+
opts
|
|
3187
|
+
);
|
|
3188
|
+
checks.push(
|
|
3189
|
+
warningCheck(
|
|
3190
|
+
codexMcpEntriesCanonical,
|
|
3191
|
+
codexMcpEntriesCanonical
|
|
3192
|
+
? "Codex CLI Sellable MCP entry canonical"
|
|
3193
|
+
: "Codex CLI Sellable MCP entry stale"
|
|
3194
|
+
)
|
|
3195
|
+
);
|
|
3196
|
+
const codexPluginMcpEntryCanonical = codexPluginMcpServerMatches(
|
|
3197
|
+
pluginMcpContent,
|
|
3198
|
+
opts
|
|
3199
|
+
);
|
|
3200
|
+
checks.push(
|
|
3201
|
+
warningCheck(
|
|
3202
|
+
codexPluginMcpEntryCanonical,
|
|
3203
|
+
codexPluginMcpEntryCanonical
|
|
3204
|
+
? "Codex Desktop plugin Sellable MCP entry canonical"
|
|
3205
|
+
: "Codex Desktop plugin Sellable MCP entry stale"
|
|
3206
|
+
)
|
|
3207
|
+
);
|
|
3030
3208
|
const hasCodexAgentRegistrations = codexCustomAgents().every((agent) =>
|
|
3031
3209
|
configContent.includes(`[agents.${agent.name}]`)
|
|
3032
3210
|
);
|
|
@@ -3330,6 +3508,7 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
3330
3508
|
{ label: "Foundation", command: "/sellable:foundation" },
|
|
3331
3509
|
{ label: "Content", command: "/sellable:content" },
|
|
3332
3510
|
{ label: "Post", command: "/sellable:create-post" },
|
|
3511
|
+
{ label: "Inbox", command: "/sellable:inbox" },
|
|
3333
3512
|
]);
|
|
3334
3513
|
console.log("");
|
|
3335
3514
|
console.log("");
|
|
@@ -3340,6 +3519,7 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
3340
3519
|
{ label: "Foundation", command: "$sellable:foundation" },
|
|
3341
3520
|
{ label: "Content", command: "$sellable:content" },
|
|
3342
3521
|
{ label: "Post", command: "$sellable:create-post" },
|
|
3522
|
+
{ label: "Inbox", command: "$sellable:inbox" },
|
|
3343
3523
|
]);
|
|
3344
3524
|
console.log("");
|
|
3345
3525
|
}
|
|
@@ -3614,14 +3794,16 @@ async function main() {
|
|
|
3614
3794
|
console.log(` apiUrl: ${apiUrl}`);
|
|
3615
3795
|
console.log(` Continue in your agent:`);
|
|
3616
3796
|
console.log(` Claude Code: /sellable:create-campaign`);
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3797
|
+
console.log(` Claude Code: /sellable:foundation`);
|
|
3798
|
+
console.log(` Claude Code: /sellable:content`);
|
|
3799
|
+
console.log(` Claude Code: /sellable:create-post`);
|
|
3800
|
+
console.log(` Claude Code: /sellable:inbox`);
|
|
3801
|
+
console.log(` Codex: $sellable:create-campaign`);
|
|
3802
|
+
console.log(` Codex: $sellable:foundation`);
|
|
3803
|
+
console.log(` Codex: $sellable:content`);
|
|
3804
|
+
console.log(` Codex: $sellable:create-post`);
|
|
3805
|
+
console.log(` Codex: $sellable:inbox`);
|
|
3806
|
+
process.exit(0);
|
|
3625
3807
|
}
|
|
3626
3808
|
if (rawArgs[0] === "prefs") {
|
|
3627
3809
|
const dryRun = rawArgs.includes("--dry-run");
|
package/lib/runtime-verify.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
3
3
|
|
|
4
4
|
const MAX_STDERR_LINES = 80;
|
|
5
5
|
const DEFAULT_VERIFY_TIMEOUT_MS = 10_000;
|
|
6
|
+
const DEFAULT_VERIFY_ATTEMPTS = 2;
|
|
7
|
+
const DEFAULT_VERIFY_RETRY_DELAY_MS = 750;
|
|
6
8
|
|
|
7
9
|
export const REQUIRED_SELLABLE_MCP_TOOLS = [
|
|
8
10
|
"get_auth_status",
|
|
@@ -125,6 +127,15 @@ export function missingRequiredTools(tools, requiredTools) {
|
|
|
125
127
|
return requiredTools.filter((name) => !available.has(name));
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
function sleep(ms) {
|
|
131
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function numberFromEnv(name, fallback) {
|
|
135
|
+
const parsed = Number(process.env[name] || "");
|
|
136
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
function textFromToolResult(result) {
|
|
129
140
|
return (result?.content || [])
|
|
130
141
|
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
@@ -291,7 +302,39 @@ async function verifyCreateCampaignSmoke({
|
|
|
291
302
|
};
|
|
292
303
|
}
|
|
293
304
|
|
|
294
|
-
|
|
305
|
+
function summarizeAttempt(result, attempt) {
|
|
306
|
+
return {
|
|
307
|
+
attempt,
|
|
308
|
+
ok: result.ok === true,
|
|
309
|
+
availableToolCount: result.availableTools?.length || 0,
|
|
310
|
+
missingToolCount: result.missingTools?.length || 0,
|
|
311
|
+
error: result.error || null,
|
|
312
|
+
createCampaignSmoke: result.createCampaignSmoke
|
|
313
|
+
? {
|
|
314
|
+
ok: result.createCampaignSmoke.ok === true,
|
|
315
|
+
error: result.createCampaignSmoke.error || null,
|
|
316
|
+
missingTools: result.createCampaignSmoke.missingTools || [],
|
|
317
|
+
calls: (result.createCampaignSmoke.calls || []).map((call) => ({
|
|
318
|
+
name: call.name,
|
|
319
|
+
ok: call.ok === true,
|
|
320
|
+
})),
|
|
321
|
+
}
|
|
322
|
+
: null,
|
|
323
|
+
stderrTail: result.stderrTail || [],
|
|
324
|
+
startedAt: result.startedAt,
|
|
325
|
+
completedAt: result.completedAt,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function shouldRetryRuntimeVerification(result, requiredTools) {
|
|
330
|
+
if (result.ok || result.skipped) return false;
|
|
331
|
+
if (result.error) return true;
|
|
332
|
+
const availableToolCount = result.availableTools?.length || 0;
|
|
333
|
+
const missingToolCount = result.missingTools?.length || 0;
|
|
334
|
+
return availableToolCount === 0 || missingToolCount === requiredTools.length;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function verifySellableMcpRuntimeOnce({
|
|
295
338
|
command,
|
|
296
339
|
args = [],
|
|
297
340
|
cwd = process.cwd(),
|
|
@@ -389,3 +432,51 @@ export async function verifySellableMcpRuntime({
|
|
|
389
432
|
completedAt: new Date().toISOString(),
|
|
390
433
|
};
|
|
391
434
|
}
|
|
435
|
+
|
|
436
|
+
export async function verifySellableMcpRuntime({
|
|
437
|
+
command,
|
|
438
|
+
args = [],
|
|
439
|
+
cwd = process.cwd(),
|
|
440
|
+
env = process.env,
|
|
441
|
+
requiredTools = REQUIRED_SELLABLE_MCP_TOOLS,
|
|
442
|
+
timeoutMs = Number(process.env.SELLABLE_VERIFY_TIMEOUT_MS || "") ||
|
|
443
|
+
DEFAULT_VERIFY_TIMEOUT_MS,
|
|
444
|
+
attempts = numberFromEnv("SELLABLE_VERIFY_ATTEMPTS", DEFAULT_VERIFY_ATTEMPTS),
|
|
445
|
+
retryDelayMs = numberFromEnv(
|
|
446
|
+
"SELLABLE_VERIFY_RETRY_DELAY_MS",
|
|
447
|
+
DEFAULT_VERIFY_RETRY_DELAY_MS
|
|
448
|
+
),
|
|
449
|
+
}) {
|
|
450
|
+
const attemptSummaries = [];
|
|
451
|
+
let lastResult = null;
|
|
452
|
+
|
|
453
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
454
|
+
const result = await verifySellableMcpRuntimeOnce({
|
|
455
|
+
command,
|
|
456
|
+
args,
|
|
457
|
+
cwd,
|
|
458
|
+
env,
|
|
459
|
+
requiredTools,
|
|
460
|
+
timeoutMs,
|
|
461
|
+
});
|
|
462
|
+
lastResult = result;
|
|
463
|
+
attemptSummaries.push(summarizeAttempt(result, attempt));
|
|
464
|
+
|
|
465
|
+
if (
|
|
466
|
+
attempt >= attempts ||
|
|
467
|
+
!shouldRetryRuntimeVerification(result, requiredTools)
|
|
468
|
+
) {
|
|
469
|
+
return {
|
|
470
|
+
...result,
|
|
471
|
+
attempts: attemptSummaries,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
await sleep(retryDelayMs);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
...lastResult,
|
|
480
|
+
attempts: attemptSummaries,
|
|
481
|
+
};
|
|
482
|
+
}
|
package/package.json
CHANGED
|
@@ -80,6 +80,14 @@ It bootstraps auth and host capabilities, then loads the internal
|
|
|
80
80
|
the internal campaign workflow prompt and `core/flow.v2.json` through MCP. Keep
|
|
81
81
|
this wrapper thin; the packaged workflow is the operational source of truth.
|
|
82
82
|
|
|
83
|
+
## Inbox Reply Tool Boundary
|
|
84
|
+
|
|
85
|
+
Inbox reply tools are outside campaign creation. If a user asks to search,
|
|
86
|
+
review, edit, or send inbox replies, treat that as a separate inbox workflow
|
|
87
|
+
using the existing product /api/v3/inbox routes. Any draft edit or send requires
|
|
88
|
+
exact approval for the workspace, sender, thread, body, tool, and expected side
|
|
89
|
+
effect before the write tool is called.
|
|
90
|
+
|
|
83
91
|
CampaignOffer state and the watch link are the customer-facing source of truth.
|
|
84
92
|
Disk artifacts are optional debug/UAT diagnostics; normal customer runs should
|
|
85
93
|
not create, link, or surface local draft files unless the user explicitly asks
|