@sellable/install 0.1.69 → 0.1.71
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 +7 -1
- package/agents/registry.json +147 -0
- package/agents/source-scout-linkedin-engagement.md +41 -0
- package/agents/source-scout-prospeo-contact.md +43 -0
- package/agents/source-scout-sales-nav.md +44 -0
- package/bin/sellable-install.mjs +228 -101
- package/package.json +2 -1
- package/skill-templates/create-campaign.md +9 -4
package/README.md
CHANGED
|
@@ -33,7 +33,13 @@ Auth is stored once at:
|
|
|
33
33
|
~/.sellable/config.json
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Claude Code and Codex are configured to launch the same packaged MCP server.
|
|
36
|
+
Claude Code and Codex are configured to launch the same packaged MCP server. The
|
|
37
|
+
installer also writes Sellable source-scout agents for both hosts from the
|
|
38
|
+
packaged `agents/` registry: Claude Code gets `source-scout-*` subagents, and
|
|
39
|
+
Codex gets `source-scout-linkedin-engagement`, `source-scout-sales-nav`, and
|
|
40
|
+
`source-scout-prospeo-contact`. The MCP exposes the same list through
|
|
41
|
+
`get_source_scout_registry`, so adding a future scout starts in one registry
|
|
42
|
+
entry instead of scattered prompt edits.
|
|
37
43
|
|
|
38
44
|
## Names
|
|
39
45
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"agents": [
|
|
4
|
+
{
|
|
5
|
+
"id": "linkedin-engagement",
|
|
6
|
+
"name": "source-scout-linkedin-engagement",
|
|
7
|
+
"promptFile": "source-scout-linkedin-engagement.md",
|
|
8
|
+
"displayName": "LinkedIn Engagement Scout",
|
|
9
|
+
"provider": "signal-discovery",
|
|
10
|
+
"lane": "linkedin-engagement",
|
|
11
|
+
"legacy": {
|
|
12
|
+
"codex": [
|
|
13
|
+
{
|
|
14
|
+
"name": "linkedin_engagement_scout",
|
|
15
|
+
"filename": "linkedin-engagement-scout.toml"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"claude": [
|
|
19
|
+
{
|
|
20
|
+
"name": "lead-explorer-signals",
|
|
21
|
+
"filename": "lead-explorer-signals.md"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"codex": {
|
|
26
|
+
"description": "Sellable lead-source scout for LinkedIn post engagement and active conversation signals.",
|
|
27
|
+
"modelReasoningEffort": "medium",
|
|
28
|
+
"sandboxMode": "read-only",
|
|
29
|
+
"nicknameCandidates": [
|
|
30
|
+
"LinkedIn Engagement Scout",
|
|
31
|
+
"Post Scout",
|
|
32
|
+
"Engager Scout"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"claude": {
|
|
36
|
+
"description": "Use proactively as a background Sellable source scout when find-leads or create-campaign needs LinkedIn post engagement, Signals, or active conversation evidence.",
|
|
37
|
+
"model": "inherit",
|
|
38
|
+
"background": true,
|
|
39
|
+
"maxTurns": 8,
|
|
40
|
+
"color": "blue",
|
|
41
|
+
"tools": [
|
|
42
|
+
"Read",
|
|
43
|
+
"Grep",
|
|
44
|
+
"Glob",
|
|
45
|
+
"mcp__sellable__get_provider_prompt",
|
|
46
|
+
"mcp__sellable__search_signals",
|
|
47
|
+
"mcp__sellable__fetch_post_engagers"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "sales-nav",
|
|
53
|
+
"name": "source-scout-sales-nav",
|
|
54
|
+
"promptFile": "source-scout-sales-nav.md",
|
|
55
|
+
"displayName": "Sales Nav Scout",
|
|
56
|
+
"provider": "sales-nav",
|
|
57
|
+
"lane": "sales-nav",
|
|
58
|
+
"legacy": {
|
|
59
|
+
"codex": [
|
|
60
|
+
{
|
|
61
|
+
"name": "sales_nav_scout",
|
|
62
|
+
"filename": "sales-nav-scout.toml"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"claude": [
|
|
66
|
+
{
|
|
67
|
+
"name": "lead-explorer-sales-nav",
|
|
68
|
+
"filename": "lead-explorer-sales-nav.md"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"codex": {
|
|
73
|
+
"description": "Sellable lead-source scout for Sales Navigator role, company, and activity filters.",
|
|
74
|
+
"modelReasoningEffort": "medium",
|
|
75
|
+
"sandboxMode": "read-only",
|
|
76
|
+
"nicknameCandidates": [
|
|
77
|
+
"Sales Nav Scout",
|
|
78
|
+
"Role Filter Scout",
|
|
79
|
+
"Activity Scout"
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"claude": {
|
|
83
|
+
"description": "Use proactively as a background Sellable source scout when find-leads or create-campaign needs Sales Navigator title, company, geography, or activity-filter evidence.",
|
|
84
|
+
"model": "inherit",
|
|
85
|
+
"background": true,
|
|
86
|
+
"maxTurns": 8,
|
|
87
|
+
"color": "cyan",
|
|
88
|
+
"tools": [
|
|
89
|
+
"Read",
|
|
90
|
+
"Grep",
|
|
91
|
+
"Glob",
|
|
92
|
+
"mcp__sellable__get_provider_prompt",
|
|
93
|
+
"mcp__sellable__lookup_sales_nav_filter",
|
|
94
|
+
"mcp__sellable__search_sales_nav"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "prospeo-contact",
|
|
100
|
+
"name": "source-scout-prospeo-contact",
|
|
101
|
+
"promptFile": "source-scout-prospeo-contact.md",
|
|
102
|
+
"displayName": "Prospeo Contact Scout",
|
|
103
|
+
"provider": "prospeo",
|
|
104
|
+
"lane": "prospeo-contact",
|
|
105
|
+
"legacy": {
|
|
106
|
+
"codex": [
|
|
107
|
+
{
|
|
108
|
+
"name": "prospeo_contact_scout",
|
|
109
|
+
"filename": "prospeo-contact-scout.toml"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"claude": [
|
|
113
|
+
{
|
|
114
|
+
"name": "lead-explorer-prospeo",
|
|
115
|
+
"filename": "lead-explorer-prospeo.md"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"codex": {
|
|
120
|
+
"description": "Sellable lead-source scout for Prospeo account/domain and broad contact expansion.",
|
|
121
|
+
"modelReasoningEffort": "medium",
|
|
122
|
+
"sandboxMode": "read-only",
|
|
123
|
+
"nicknameCandidates": [
|
|
124
|
+
"Prospeo Contact Scout",
|
|
125
|
+
"Domain Scout",
|
|
126
|
+
"Contact Scout"
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
"claude": {
|
|
130
|
+
"description": "Use proactively as a background Sellable source scout when find-leads or create-campaign needs Prospeo account, domain-list, CSV-domain, or verified-contact evidence.",
|
|
131
|
+
"model": "inherit",
|
|
132
|
+
"background": true,
|
|
133
|
+
"maxTurns": 8,
|
|
134
|
+
"color": "green",
|
|
135
|
+
"tools": [
|
|
136
|
+
"Read",
|
|
137
|
+
"Grep",
|
|
138
|
+
"Glob",
|
|
139
|
+
"mcp__sellable__get_provider_prompt",
|
|
140
|
+
"mcp__sellable__load_csv_domains",
|
|
141
|
+
"mcp__sellable__save_domain_filters",
|
|
142
|
+
"mcp__sellable__search_prospeo"
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
You are the LinkedIn Engagement Scout for Sellable find-leads.
|
|
2
|
+
|
|
3
|
+
Your job is to test whether active LinkedIn posts and engagers can produce a warm first-send list for the campaign. Work only on this source lane. Do not import leads, create campaigns, write campaign artifacts, draft messages, ask the user questions, or make the final source decision.
|
|
4
|
+
|
|
5
|
+
Required first step:
|
|
6
|
+
|
|
7
|
+
- Load the canonical provider prompt before searching: `get_provider_prompt({ provider: "signal-discovery", confirmed: true })`.
|
|
8
|
+
|
|
9
|
+
Use the inherited Sellable MCP tools when available:
|
|
10
|
+
|
|
11
|
+
- `search_signals` to find recent post lanes.
|
|
12
|
+
- `fetch_post_engagers` to sample engagers from selected posts.
|
|
13
|
+
|
|
14
|
+
Process:
|
|
15
|
+
|
|
16
|
+
1. Read the campaign brief, kickoff doc, or lane prompt supplied by the parent.
|
|
17
|
+
2. Search 3-5 keyword/topic lanes, favoring fresh posts from the last 7-14 days.
|
|
18
|
+
3. Select 3-5 promising posts when available.
|
|
19
|
+
4. Fetch or sample engagers for selected posts and score rough ICP fit from visible headline/display-name cues only. Do not enrich people during viability estimation.
|
|
20
|
+
5. Estimate usable prospects per selected post from sampled pass rate. If the sample is good but volume is low, say how many more similar posts should be added or scraped.
|
|
21
|
+
6. Return false positives and dead ends explicitly.
|
|
22
|
+
|
|
23
|
+
Return a concise structured result with:
|
|
24
|
+
|
|
25
|
+
- `source_lane`
|
|
26
|
+
- `provider_prompt_loaded`
|
|
27
|
+
- `keyword_lanes` with timeframe, raw posts found, finalist posts reviewed
|
|
28
|
+
- `selected_posts` with URL/title, author/topic, age, engager count, sampled engagers, good fits as n/N, estimated usable prospects per post, use/discard
|
|
29
|
+
- `sample_leads`, if any
|
|
30
|
+
- `estimated_good_fit_range`
|
|
31
|
+
- `expected_reply_rate_range`, directional if inferred
|
|
32
|
+
- `false_positive_patterns`
|
|
33
|
+
- `recommendation`
|
|
34
|
+
- `confidence`
|
|
35
|
+
|
|
36
|
+
Evidence standards:
|
|
37
|
+
|
|
38
|
+
- Do not trust raw post volume without inspecting finalist post quality.
|
|
39
|
+
- Prefer sample-based pass rates over intuition.
|
|
40
|
+
- If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
|
|
41
|
+
- Keep LinkedIn Engagement viable when selected posts can produce roughly 150+ ICP-fit warm prospects before final filtering, even if Sales Nav is more scalable.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
You are the Prospeo Contact Scout for Sellable find-leads.
|
|
2
|
+
|
|
3
|
+
Your job is to test whether Prospeo can produce verified-contact scale for the campaign through account/domain targeting or broad persona expansion. Work only on this source lane. Do not import leads, create campaigns, write campaign artifacts, draft messages, ask the user questions, or make the final source decision.
|
|
4
|
+
|
|
5
|
+
Required first step:
|
|
6
|
+
|
|
7
|
+
- Load the canonical provider prompt before searching: `get_provider_prompt({ provider: "prospeo", confirmed: true })`.
|
|
8
|
+
|
|
9
|
+
Use the inherited Sellable MCP tools when available:
|
|
10
|
+
|
|
11
|
+
- `load_csv_domains` when the parent supplies a CSV on disk and no `domainFilterId` exists.
|
|
12
|
+
- `save_domain_filters` when the parent supplies pasted/raw include or exclude domains and no `domainFilterId` exists.
|
|
13
|
+
- `search_prospeo` for campaignless people previews.
|
|
14
|
+
|
|
15
|
+
Process:
|
|
16
|
+
|
|
17
|
+
1. Read the campaign brief, source intake, kickoff doc, or lane prompt supplied by the parent.
|
|
18
|
+
2. Identify whether this is domain/account targeting or broad persona expansion.
|
|
19
|
+
3. For domain targeting, use or create the standalone `domainFilterId` before searching; never pass raw domains directly into `search_prospeo`.
|
|
20
|
+
4. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear.
|
|
21
|
+
5. Call out that Prospeo gives contact/account coverage but usually weaker LinkedIn intent than LinkedIn Engagement or Sales Nav activity slices.
|
|
22
|
+
|
|
23
|
+
Return a concise structured result with:
|
|
24
|
+
|
|
25
|
+
- `source_lane`
|
|
26
|
+
- `provider_prompt_loaded`
|
|
27
|
+
- `mode`
|
|
28
|
+
- `domain_filter_or_account_inputs`
|
|
29
|
+
- `exact_search_recipe`
|
|
30
|
+
- `raw_result_count`
|
|
31
|
+
- `sampled_people` and good fits as n/N
|
|
32
|
+
- `estimated_good_fit_range_after_cleanup`
|
|
33
|
+
- `expected_reply_rate_range`, directional if inferred
|
|
34
|
+
- `sample_leads`
|
|
35
|
+
- `false_positive_patterns`
|
|
36
|
+
- `recommendation`
|
|
37
|
+
- `confidence`
|
|
38
|
+
|
|
39
|
+
Evidence standards:
|
|
40
|
+
|
|
41
|
+
- Never pass raw domains, company website arrays, or company-name arrays into `search_prospeo`.
|
|
42
|
+
- If the user supplied company names rather than domains, report that domain resolution is required before this lane can run safely.
|
|
43
|
+
- Treat Prospeo as an account/contact coverage lane, not as proof of fresh LinkedIn intent.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
You are the Sales Nav Scout for Sellable find-leads.
|
|
2
|
+
|
|
3
|
+
Your job is to test whether Sales Navigator filters can produce a scalable, high-fit lead pool for the campaign. Work only on this source lane. Do not import leads, create campaigns, write campaign artifacts, draft messages, ask the user questions, or make the final source decision.
|
|
4
|
+
|
|
5
|
+
Required first step:
|
|
6
|
+
|
|
7
|
+
- Load the canonical provider prompt before searching: `get_provider_prompt({ provider: "sales-nav", confirmed: true })`.
|
|
8
|
+
|
|
9
|
+
Use the inherited Sellable MCP tools when available:
|
|
10
|
+
|
|
11
|
+
- `lookup_sales_nav_filter` before any dynamic Sales Nav filter.
|
|
12
|
+
- `search_sales_nav` for campaignless preview searches.
|
|
13
|
+
|
|
14
|
+
Process:
|
|
15
|
+
|
|
16
|
+
1. Read the campaign brief, kickoff doc, or lane prompt supplied by the parent.
|
|
17
|
+
2. Preserve target role names with `CURRENT_TITLE` lookups; do not rely on seniority alone when the brief names concrete roles.
|
|
18
|
+
3. When `lookup_sales_nav_filter` returns multiple title options, choose the closest semantic title match instead of the first result.
|
|
19
|
+
4. Build a broad-but-reasonable baseline from role/title, geography, company size, industry/account context, and recent LinkedIn activity when relevant.
|
|
20
|
+
5. Run the baseline plus 1-2 refinements if the first pass is noisy or under-scaled.
|
|
21
|
+
6. Verify filters actually applied: returned search URL contains filters, first-page rows match the intended lane, and result count does not look like an unfiltered pool.
|
|
22
|
+
|
|
23
|
+
Return a concise structured result with:
|
|
24
|
+
|
|
25
|
+
- `source_lane`
|
|
26
|
+
- `provider_prompt_loaded`
|
|
27
|
+
- `exact_filter_recipe`
|
|
28
|
+
- `lookup_ids_used`
|
|
29
|
+
- `raw_result_count`
|
|
30
|
+
- `sampled_people` and good fits as n/N
|
|
31
|
+
- `estimated_good_fit_range_after_cleanup`
|
|
32
|
+
- `expected_acceptance_rate_range`, directional if inferred
|
|
33
|
+
- `expected_reply_rate_range`, directional if inferred
|
|
34
|
+
- `sample_leads`
|
|
35
|
+
- `false_positive_patterns`
|
|
36
|
+
- `recommendation`
|
|
37
|
+
- `confidence`
|
|
38
|
+
|
|
39
|
+
Evidence standards:
|
|
40
|
+
|
|
41
|
+
- Optimize for a useful prospect pool, not max volume at any cost.
|
|
42
|
+
- Bias toward `POSTED_ON_LINKEDIN` for reply-likelihood when the pool still has enough scale.
|
|
43
|
+
- Do not hand-wave missing filter IDs.
|
|
44
|
+
- If Sales Nav returns a giant unfiltered pool, discard that result and retry with valid filters before recommending it.
|
package/bin/sellable-install.mjs
CHANGED
|
@@ -465,6 +465,7 @@ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
|
|
|
465
465
|
"mcp__sellable__get_subskill_asset",
|
|
466
466
|
"mcp__sellable__search_subskill_prompts",
|
|
467
467
|
"mcp__sellable__get_provider_prompt",
|
|
468
|
+
"mcp__sellable__get_source_scout_registry",
|
|
468
469
|
"mcp__sellable__get_message_prompt",
|
|
469
470
|
"mcp__sellable__get_active_workspace",
|
|
470
471
|
"mcp__sellable__list_senders",
|
|
@@ -1089,110 +1090,129 @@ function codexPluginSkills() {
|
|
|
1089
1090
|
];
|
|
1090
1091
|
}
|
|
1091
1092
|
|
|
1093
|
+
function installerPackageRoot() {
|
|
1094
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function agentTemplateRoot() {
|
|
1098
|
+
const packagedRoot = join(installerPackageRoot(), "agents");
|
|
1099
|
+
if (existsSync(join(packagedRoot, "registry.json"))) {
|
|
1100
|
+
return packagedRoot;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Dev fallback for running the installer directly from the repo before
|
|
1104
|
+
// sync-skill-templates has copied the package templates.
|
|
1105
|
+
const repoRoot = join(installerPackageRoot(), "..", "..");
|
|
1106
|
+
const canonicalRoot = join(repoRoot, "mcp", "sellable", "agents");
|
|
1107
|
+
if (existsSync(join(canonicalRoot, "registry.json"))) {
|
|
1108
|
+
return canonicalRoot;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`Sellable source scout agent templates missing. Expected ${join(
|
|
1113
|
+
packagedRoot,
|
|
1114
|
+
"registry.json"
|
|
1115
|
+
)}`
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function loadCanonicalAgents() {
|
|
1120
|
+
const root = agentTemplateRoot();
|
|
1121
|
+
const registry = JSON.parse(readFileSync(join(root, "registry.json"), "utf8"));
|
|
1122
|
+
if (!Array.isArray(registry.agents)) {
|
|
1123
|
+
throw new Error("Sellable agent registry is missing agents array.");
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return registry.agents.map((agent) => {
|
|
1127
|
+
const promptPath = join(root, agent.promptFile || "");
|
|
1128
|
+
if (!agent.id || !agent.name || !agent.promptFile || !existsSync(promptPath)) {
|
|
1129
|
+
throw new Error(`Invalid Sellable agent registry entry: ${agent.id || "unknown"}`);
|
|
1130
|
+
}
|
|
1131
|
+
if (agent.codex?.name && agent.codex.name !== agent.name) {
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
`Sellable agent ${agent.id} has divergent Codex name ${agent.codex.name}; use canonical name ${agent.name}.`
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
if (agent.claude?.name && agent.claude.name !== agent.name) {
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
`Sellable agent ${agent.id} has divergent Claude name ${agent.claude.name}; use canonical name ${agent.name}.`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return {
|
|
1143
|
+
...agent,
|
|
1144
|
+
prompt: readFileSync(promptPath, "utf8").trimEnd(),
|
|
1145
|
+
};
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function codexAgentFilename(agent) {
|
|
1150
|
+
return agent.codex?.filename || `${agent.name}.toml`;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function claudeAgentFilename(agent) {
|
|
1154
|
+
return agent.claude?.filename || `${agent.name}.md`;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function legacyCodexCustomAgents() {
|
|
1158
|
+
return loadCanonicalAgents().flatMap((agent) => agent.legacy?.codex || []);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function legacyClaudeCustomAgents() {
|
|
1162
|
+
return loadCanonicalAgents().flatMap((agent) => agent.legacy?.claude || []);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function tomlArray(values) {
|
|
1166
|
+
return `[${values.map((value) => quoteToml(value)).join(", ")}]`;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function tomlMultilineString(value) {
|
|
1170
|
+
const escaped = String(value).replace(/\\/g, "\\\\").replace(/"""/g, '\\"""');
|
|
1171
|
+
return `"""\n${escaped}\n"""`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function generateCodexAgentToml(agent) {
|
|
1175
|
+
const codex = agent.codex;
|
|
1176
|
+
return `name = ${quoteToml(agent.name)}
|
|
1177
|
+
description = ${quoteToml(codex.description)}
|
|
1178
|
+
model_reasoning_effort = ${quoteToml(codex.modelReasoningEffort || "medium")}
|
|
1179
|
+
sandbox_mode = ${quoteToml(codex.sandboxMode || "read-only")}
|
|
1180
|
+
nickname_candidates = ${tomlArray(codex.nicknameCandidates || [agent.displayName])}
|
|
1181
|
+
developer_instructions = ${tomlMultilineString(agent.prompt)}
|
|
1182
|
+
`;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1092
1185
|
function codexCustomAgents() {
|
|
1093
|
-
|
|
1186
|
+
return loadCanonicalAgents().map((agent) => ({
|
|
1187
|
+
name: agent.name,
|
|
1188
|
+
filename: codexAgentFilename(agent),
|
|
1189
|
+
content: generateCodexAgentToml(agent),
|
|
1190
|
+
}));
|
|
1191
|
+
}
|
|
1094
1192
|
|
|
1095
|
-
|
|
1193
|
+
function generateClaudeAgentMd(agent) {
|
|
1194
|
+
const claude = agent.claude;
|
|
1195
|
+
return `---
|
|
1196
|
+
name: ${agent.name}
|
|
1197
|
+
description: ${JSON.stringify(claude.description)}
|
|
1198
|
+
tools: ${(claude.tools || []).join(", ")}
|
|
1199
|
+
model: ${claude.model || "inherit"}
|
|
1200
|
+
background: ${claude.background === true ? "true" : "false"}
|
|
1201
|
+
maxTurns: ${Number.isFinite(claude.maxTurns) ? claude.maxTurns : 8}
|
|
1202
|
+
color: ${claude.color || "blue"}
|
|
1203
|
+
---
|
|
1096
1204
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
Your lane is LinkedIn Engagement. Internally, use the signal-discovery provider prompt and the Signals tools when available.
|
|
1109
|
-
|
|
1110
|
-
Use the inherited Sellable MCP tools when available:
|
|
1111
|
-
- get_provider_prompt for signal-discovery before searching
|
|
1112
|
-
- search_signals to find recent post lanes
|
|
1113
|
-
- fetch_post_engagers to sample engagers from selected posts
|
|
1114
|
-
|
|
1115
|
-
Process:
|
|
1116
|
-
1. Search 3-5 keyword/topic lanes, favoring fresh posts from the last 7-14 days.
|
|
1117
|
-
2. Select 3-5 promising posts when available.
|
|
1118
|
-
3. Fetch or sample engagers for selected posts and score rough ICP fit from visible headline/display cues.
|
|
1119
|
-
4. Estimate usable prospects per selected post from sampled pass rate.
|
|
1120
|
-
5. If the sample is good but volume is low, say how many more similar posts should be added or scraped.
|
|
1121
|
-
|
|
1122
|
-
Return:
|
|
1123
|
-
- keyword lanes searched, timeframe, raw posts found, finalist posts reviewed
|
|
1124
|
-
- selected post URL/title, author/topic, age, engager count, sampled engagers, good fits as n/N, estimated usable prospects per post, use/discard
|
|
1125
|
-
- sample leads if any
|
|
1126
|
-
- estimated good-fit range, directional reply-rate range, false positives, recommendation, confidence
|
|
1127
|
-
"""
|
|
1128
|
-
`,
|
|
1129
|
-
},
|
|
1130
|
-
{
|
|
1131
|
-
filename: "sales-nav-scout.toml",
|
|
1132
|
-
content: `name = "sales_nav_scout"
|
|
1133
|
-
description = "Sellable lead-source scout for Sales Navigator role, company, and activity filters."
|
|
1134
|
-
model_reasoning_effort = "medium"
|
|
1135
|
-
sandbox_mode = "read-only"
|
|
1136
|
-
nickname_candidates = ["Sales Nav Scout", "Role Filter Scout", "Activity Scout"]
|
|
1137
|
-
developer_instructions = """
|
|
1138
|
-
${commonPrefix}
|
|
1139
|
-
|
|
1140
|
-
Your lane is Sales Nav.
|
|
1141
|
-
|
|
1142
|
-
Use the inherited Sellable MCP tools when available:
|
|
1143
|
-
- get_provider_prompt for sales-nav before searching
|
|
1144
|
-
- lookup_sales_nav_filter before dynamic filters
|
|
1145
|
-
- search_sales_nav for campaignless preview searches
|
|
1146
|
-
|
|
1147
|
-
Process:
|
|
1148
|
-
1. Preserve target role names with CURRENT_TITLE lookups; do not rely on seniority alone when the brief names concrete roles.
|
|
1149
|
-
2. Build a broad-but-reasonable baseline from role/title, geography, company size, industry/account context, and recent LinkedIn activity when relevant.
|
|
1150
|
-
3. Run the baseline plus 1-2 refinements if the first pass is noisy or under-scaled.
|
|
1151
|
-
4. Verify filters actually applied: search URL contains filters, first-page rows match the intended lane, and result count does not look like an unfiltered pool.
|
|
1152
|
-
|
|
1153
|
-
Return:
|
|
1154
|
-
- exact filter recipe and lookup IDs used
|
|
1155
|
-
- raw result count, sampled people, good fits as n/N
|
|
1156
|
-
- estimated good-fit range after cleanup
|
|
1157
|
-
- directional acceptance/reply-rate ranges
|
|
1158
|
-
- sample leads, false positives, recommendation, confidence
|
|
1159
|
-
"""
|
|
1160
|
-
`,
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
filename: "prospeo-contact-scout.toml",
|
|
1164
|
-
content: `name = "prospeo_contact_scout"
|
|
1165
|
-
description = "Sellable lead-source scout for Prospeo account/domain and broad contact expansion."
|
|
1166
|
-
model_reasoning_effort = "medium"
|
|
1167
|
-
sandbox_mode = "read-only"
|
|
1168
|
-
nickname_candidates = ["Prospeo Contact Scout", "Domain Scout", "Contact Scout"]
|
|
1169
|
-
developer_instructions = """
|
|
1170
|
-
${commonPrefix}
|
|
1171
|
-
|
|
1172
|
-
Your lane is Prospeo Contact.
|
|
1173
|
-
|
|
1174
|
-
Use the inherited Sellable MCP tools when available:
|
|
1175
|
-
- get_provider_prompt for prospeo before searching
|
|
1176
|
-
- load_csv_domains when the parent supplies a CSV or domain list and no domainFilterId exists
|
|
1177
|
-
- search_prospeo for campaignless people previews
|
|
1178
|
-
|
|
1179
|
-
Process:
|
|
1180
|
-
1. Identify whether this is domain/account targeting or broad persona expansion.
|
|
1181
|
-
2. For domain targeting, use or create the standalone domainFilterId before searching; never pass raw domains directly into search_prospeo.
|
|
1182
|
-
3. Run the narrowest useful Prospeo people preview and 1-2 refinements if quality or scale is unclear.
|
|
1183
|
-
4. Call out that Prospeo gives contact/account coverage but usually weaker LinkedIn intent than LinkedIn Engagement or Sales Nav activity slices.
|
|
1184
|
-
|
|
1185
|
-
Return:
|
|
1186
|
-
- mode and domain/account inputs
|
|
1187
|
-
- exact search recipe
|
|
1188
|
-
- raw result count, sampled people, good fits as n/N
|
|
1189
|
-
- estimated good-fit range after cleanup
|
|
1190
|
-
- directional reply-rate range
|
|
1191
|
-
- sample leads, false positives, recommendation, confidence
|
|
1192
|
-
"""
|
|
1193
|
-
`,
|
|
1194
|
-
},
|
|
1195
|
-
];
|
|
1205
|
+
${agent.prompt}
|
|
1206
|
+
`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function claudeCustomAgents() {
|
|
1210
|
+
return loadCanonicalAgents().map((agent) => ({
|
|
1211
|
+
name: agent.name,
|
|
1212
|
+
filename: claudeAgentFilename(agent),
|
|
1213
|
+
tools: agent.claude.tools || [],
|
|
1214
|
+
content: generateClaudeAgentMd(agent),
|
|
1215
|
+
}));
|
|
1196
1216
|
}
|
|
1197
1217
|
|
|
1198
1218
|
function writeCodexPluginSkills(pluginRoot, opts) {
|
|
@@ -1214,6 +1234,27 @@ function writeCodexCustomAgents(home, opts) {
|
|
|
1214
1234
|
for (const agent of codexCustomAgents()) {
|
|
1215
1235
|
writeFile(join(home, "agents", agent.filename), agent.content, opts);
|
|
1216
1236
|
}
|
|
1237
|
+
for (const agent of legacyCodexCustomAgents()) {
|
|
1238
|
+
const legacyPath = join(home, "agents", agent.filename);
|
|
1239
|
+
logVerbose(`${C.grey}Removing legacy Codex scout agent ${legacyPath}${C.reset}`);
|
|
1240
|
+
if (!opts.dryRun) rmSync(legacyPath, { force: true });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function claudeHome() {
|
|
1245
|
+
return join(homedir(), ".claude");
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function writeClaudeCustomAgents(opts) {
|
|
1249
|
+
const home = claudeHome();
|
|
1250
|
+
for (const agent of claudeCustomAgents()) {
|
|
1251
|
+
writeFile(join(home, "agents", agent.filename), agent.content, opts);
|
|
1252
|
+
}
|
|
1253
|
+
for (const agent of legacyClaudeCustomAgents()) {
|
|
1254
|
+
const legacyPath = join(home, "agents", agent.filename);
|
|
1255
|
+
logVerbose(`${C.grey}Removing legacy Claude scout agent ${legacyPath}${C.reset}`);
|
|
1256
|
+
if (!opts.dryRun) rmSync(legacyPath, { force: true });
|
|
1257
|
+
}
|
|
1217
1258
|
}
|
|
1218
1259
|
|
|
1219
1260
|
function installCodexDesktopPlugin(opts) {
|
|
@@ -1322,6 +1363,24 @@ enabled = false`
|
|
|
1322
1363
|
"default_mode_request_user_input",
|
|
1323
1364
|
true
|
|
1324
1365
|
);
|
|
1366
|
+
content = upsertTomlTable(
|
|
1367
|
+
content,
|
|
1368
|
+
"agents",
|
|
1369
|
+
`[agents]
|
|
1370
|
+
max_threads = 6
|
|
1371
|
+
max_depth = 1`
|
|
1372
|
+
);
|
|
1373
|
+
for (const agent of legacyCodexCustomAgents()) {
|
|
1374
|
+
content = removeTomlSection(content, `agents.${agent.name}`);
|
|
1375
|
+
}
|
|
1376
|
+
for (const agent of codexCustomAgents()) {
|
|
1377
|
+
content = upsertTomlTable(
|
|
1378
|
+
content,
|
|
1379
|
+
`agents.${agent.name}`,
|
|
1380
|
+
`[agents.${agent.name}]
|
|
1381
|
+
config_file = ${quoteToml(join(home, "agents", agent.filename))}`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1325
1384
|
writeFileSync(configPath, `${content.trimEnd()}\n`, { mode: 0o600 });
|
|
1326
1385
|
} else {
|
|
1327
1386
|
logVerbose(
|
|
@@ -1334,6 +1393,7 @@ enabled = false`
|
|
|
1334
1393
|
`${C.grey}+ enable [features].default_mode_request_user_input in ${configPath}${C.reset}`
|
|
1335
1394
|
);
|
|
1336
1395
|
logVerbose(`${C.grey}+ write Codex custom scout agents in ${home}/agents${C.reset}`);
|
|
1396
|
+
logVerbose(`${C.grey}+ register Codex source scout agents in ${configPath}${C.reset}`);
|
|
1337
1397
|
}
|
|
1338
1398
|
|
|
1339
1399
|
return {
|
|
@@ -1397,12 +1457,14 @@ function installClaude(opts) {
|
|
|
1397
1457
|
}
|
|
1398
1458
|
throw new Error(message);
|
|
1399
1459
|
}
|
|
1460
|
+
writeClaudeCustomAgents(opts);
|
|
1400
1461
|
if (opts.server === "hosted") {
|
|
1401
1462
|
run(
|
|
1402
1463
|
"claude",
|
|
1403
1464
|
["mcp", "add", "--transport", "http", "sellable", opts.hostedUrl],
|
|
1404
1465
|
opts
|
|
1405
1466
|
);
|
|
1467
|
+
patchClaudeAlwaysLoad(opts);
|
|
1406
1468
|
return true;
|
|
1407
1469
|
}
|
|
1408
1470
|
const [command, args] = mcpCommand(opts);
|
|
@@ -1517,6 +1579,30 @@ function verify(opts) {
|
|
|
1517
1579
|
? "Claude CLI present"
|
|
1518
1580
|
: "Claude CLI missing",
|
|
1519
1581
|
});
|
|
1582
|
+
const claudeAgentPaths = claudeCustomAgents().map((agent) =>
|
|
1583
|
+
join(claudeHome(), "agents", agent.filename)
|
|
1584
|
+
);
|
|
1585
|
+
const hasClaudeScouts = claudeAgentPaths.every((agentPath) =>
|
|
1586
|
+
existsSync(agentPath)
|
|
1587
|
+
);
|
|
1588
|
+
checks.push({
|
|
1589
|
+
ok: hasClaudeScouts,
|
|
1590
|
+
label: hasClaudeScouts
|
|
1591
|
+
? "Claude custom scout agents present"
|
|
1592
|
+
: "Claude custom scout agents missing",
|
|
1593
|
+
});
|
|
1594
|
+
const claudeAgentsHaveTools = claudeCustomAgents().every((agent) => {
|
|
1595
|
+
const agentPath = join(claudeHome(), "agents", agent.filename);
|
|
1596
|
+
if (!existsSync(agentPath)) return false;
|
|
1597
|
+
const content = readFileSync(agentPath, "utf8");
|
|
1598
|
+
return agent.tools.every((tool) => content.includes(tool));
|
|
1599
|
+
});
|
|
1600
|
+
checks.push({
|
|
1601
|
+
ok: claudeAgentsHaveTools,
|
|
1602
|
+
label: claudeAgentsHaveTools
|
|
1603
|
+
? "Claude scout MCP tool allowlists present"
|
|
1604
|
+
: "Claude scout MCP tool allowlists missing",
|
|
1605
|
+
});
|
|
1520
1606
|
}
|
|
1521
1607
|
if (opts.host === "codex" || opts.host === "all") {
|
|
1522
1608
|
checks.push({
|
|
@@ -1572,6 +1658,15 @@ function verify(opts) {
|
|
|
1572
1658
|
const configContent = existsSync(configPath)
|
|
1573
1659
|
? readFileSync(configPath, "utf8")
|
|
1574
1660
|
: "";
|
|
1661
|
+
const hasCodexAgentRegistrations = codexCustomAgents().every((agent) =>
|
|
1662
|
+
configContent.includes(`[agents.${agent.name}]`)
|
|
1663
|
+
);
|
|
1664
|
+
checks.push({
|
|
1665
|
+
ok: hasCodexAgentRegistrations,
|
|
1666
|
+
label: hasCodexAgentRegistrations
|
|
1667
|
+
? "Codex custom scout agents registered"
|
|
1668
|
+
: "Codex custom scout agents unregistered",
|
|
1669
|
+
});
|
|
1575
1670
|
const hasFlag = configContent.includes(
|
|
1576
1671
|
"default_mode_request_user_input = true"
|
|
1577
1672
|
);
|
|
@@ -1720,6 +1815,9 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
1720
1815
|
console.log(` ${C.green}✓${C.reset} Skills installed`);
|
|
1721
1816
|
console.log(` ${C.green}✓${C.reset} Codex source scout agents installed`);
|
|
1722
1817
|
}
|
|
1818
|
+
if (hasClaude) {
|
|
1819
|
+
console.log(` ${C.green}✓${C.reset} Claude source scout agents installed`);
|
|
1820
|
+
}
|
|
1723
1821
|
if (authReused) {
|
|
1724
1822
|
console.log(
|
|
1725
1823
|
` ${C.green}✓${C.reset} Existing session detected — no need to sign in again`
|
|
@@ -1797,6 +1895,9 @@ function runUninstall() {
|
|
|
1797
1895
|
after = removeTomlSection(after, "marketplaces.sellable");
|
|
1798
1896
|
after = removeTomlSection(after, 'plugins."sellable@sellable"');
|
|
1799
1897
|
after = removeTomlSection(after, 'plugins."sellable@sellable-local"');
|
|
1898
|
+
for (const agent of [...codexCustomAgents(), ...legacyCodexCustomAgents()]) {
|
|
1899
|
+
after = removeTomlSection(after, `agents.${agent.name}`);
|
|
1900
|
+
}
|
|
1800
1901
|
// Collapse 3+ blank lines that the removals may leave behind.
|
|
1801
1902
|
after = after.replace(/\n{3,}/g, "\n\n");
|
|
1802
1903
|
if (after !== before) {
|
|
@@ -1817,6 +1918,19 @@ function runUninstall() {
|
|
|
1817
1918
|
skipped.push(`Codex config not found at ${codexConfigPath}`);
|
|
1818
1919
|
}
|
|
1819
1920
|
|
|
1921
|
+
for (const agent of [...codexCustomAgents(), ...legacyCodexCustomAgents()]) {
|
|
1922
|
+
const agentPath = join(codexHome(), "agents", agent.filename);
|
|
1923
|
+
if (!existsSync(agentPath)) continue;
|
|
1924
|
+
try {
|
|
1925
|
+
rmSync(agentPath, { force: true });
|
|
1926
|
+
removed.push(`${agentPath}`);
|
|
1927
|
+
} catch (err) {
|
|
1928
|
+
console.log(
|
|
1929
|
+
` ${C.yellow}!${C.reset} Could not remove ${agentPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1820
1934
|
// 2) Claude Code MCP removal
|
|
1821
1935
|
if (commandExists("claude")) {
|
|
1822
1936
|
const r = spawnSync("claude", ["mcp", "remove", "sellable"], {
|
|
@@ -1831,6 +1945,19 @@ function runUninstall() {
|
|
|
1831
1945
|
skipped.push(`Claude Code CLI not found`);
|
|
1832
1946
|
}
|
|
1833
1947
|
|
|
1948
|
+
for (const agent of [...claudeCustomAgents(), ...legacyClaudeCustomAgents()]) {
|
|
1949
|
+
const agentPath = join(claudeHome(), "agents", agent.filename);
|
|
1950
|
+
if (!existsSync(agentPath)) continue;
|
|
1951
|
+
try {
|
|
1952
|
+
rmSync(agentPath, { force: true });
|
|
1953
|
+
removed.push(`${agentPath}`);
|
|
1954
|
+
} catch (err) {
|
|
1955
|
+
console.log(
|
|
1956
|
+
` ${C.yellow}!${C.reset} Could not remove ${agentPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1834
1961
|
// 3) Surgical removal of Sellable-installed artifacts inside ~/.sellable/
|
|
1835
1962
|
const sellableDir = join(homedir(), ".sellable");
|
|
1836
1963
|
if (existsSync(sellableDir)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sellable/install",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.71",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One-command installer for Sellable MCP in Claude Code and Codex",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin",
|
|
15
|
+
"agents",
|
|
15
16
|
"skill-templates",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
@@ -11,6 +11,7 @@ allowed-tools:
|
|
|
11
11
|
- mcp__sellable__get_subskill_asset
|
|
12
12
|
- mcp__sellable__search_subskill_prompts
|
|
13
13
|
- mcp__sellable__get_provider_prompt
|
|
14
|
+
- mcp__sellable__get_source_scout_registry
|
|
14
15
|
- mcp__sellable__get_message_prompt
|
|
15
16
|
- mcp__sellable__get_active_workspace
|
|
16
17
|
- mcp__sellable__list_senders
|
|
@@ -102,11 +103,15 @@ scout those angles as independent branches when the host can actually do it:
|
|
|
102
103
|
LinkedIn Engagement / active post engagers (internal `signal-discovery`
|
|
103
104
|
provider prompt), Sales Nav / title + company filters, and Prospeo Contact /
|
|
104
105
|
domains only when relevant. In Codex, explicitly spawn the named custom scouts
|
|
105
|
-
`
|
|
106
|
+
`source-scout-linkedin-engagement`, `source-scout-sales-nav`, and `source-scout-prospeo-contact` for
|
|
106
107
|
the credible lanes; Codex does not infer subagent fan-out from generic source
|
|
107
|
-
comparison wording. In Claude Code,
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
comparison wording. In Claude Code, invoke the generated `source-scout-*`
|
|
109
|
+
Task/Agent subagents for all credible lanes in one assistant message; the
|
|
110
|
+
installer writes them from the same canonical Sellable agent registry with
|
|
111
|
+
explicit Sellable MCP tool allowlists. The create-campaign-v2 subskill calls
|
|
112
|
+
`get_source_scout_registry` before dispatch so the current registry, not this
|
|
113
|
+
copy, is the runtime source of truth. If the host runs them sequentially, do not
|
|
114
|
+
claim they ran in parallel. In chat, call the downstream copy stage `message generation`;
|
|
110
115
|
`message-validation.md` is only an internal proof artifact.
|
|
111
116
|
|
|
112
117
|
Use rendered Markdown for user review surfaces, not fenced code blocks. Keep
|