neuron-inspector 0.2.1 → 0.3.0
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/dist/compound-tools.d.ts +19 -0
- package/dist/compound-tools.js +523 -0
- package/dist/compound-tools.js.map +1 -0
- package/dist/server.js +67 -3
- package/dist/server.js.map +1 -1
- package/package.json +3 -2
- package/recipes/linkedin-outreach/agent.md +217 -0
- package/recipes/linkedin-outreach/learnings.md +37 -0
- package/recipes/linkedin-outreach/recipe.yaml +99 -0
- package/recipes/planner/agent.md +243 -0
- package/recipes/planner/learnings.md +27 -0
- package/recipes/planner/recipe.yaml +62 -0
package/dist/server.js
CHANGED
|
@@ -1,9 +1,51 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { WebSocketServer, WebSocket } from "ws";
|
|
4
|
+
import { z } from "zod";
|
|
4
5
|
import { PendingCalls } from "./correlation.js";
|
|
5
6
|
import { TOOLS, toolToPrimitive } from "./tools.js";
|
|
6
7
|
import { RECIPE_TOOLS, handleRecipeTool } from "./recipe-tools.js";
|
|
8
|
+
import { COMPOUND_TOOLS, handleCompoundTool } from "./compound-tools.js";
|
|
9
|
+
function propToZod(p) {
|
|
10
|
+
let zt;
|
|
11
|
+
if (Array.isArray(p?.enum) && p.enum.length > 0) {
|
|
12
|
+
zt = z.enum(p.enum);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
switch (p?.type) {
|
|
16
|
+
case "number":
|
|
17
|
+
case "integer":
|
|
18
|
+
zt = z.number();
|
|
19
|
+
break;
|
|
20
|
+
case "boolean":
|
|
21
|
+
zt = z.boolean();
|
|
22
|
+
break;
|
|
23
|
+
case "array":
|
|
24
|
+
zt = z.array(p.items ? propToZod(p.items) : z.any());
|
|
25
|
+
break;
|
|
26
|
+
case "object":
|
|
27
|
+
zt = p.properties ? z.object(shapeFromSchema(p)) : z.record(z.string(), z.any());
|
|
28
|
+
break;
|
|
29
|
+
case "string":
|
|
30
|
+
zt = z.string();
|
|
31
|
+
break;
|
|
32
|
+
default: zt = z.any();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (p?.description)
|
|
36
|
+
zt = zt.describe(p.description);
|
|
37
|
+
return zt;
|
|
38
|
+
}
|
|
39
|
+
function shapeFromSchema(schema) {
|
|
40
|
+
const props = schema?.properties ?? {};
|
|
41
|
+
const required = Array.isArray(schema?.required) ? schema.required : [];
|
|
42
|
+
const shape = {};
|
|
43
|
+
for (const [k, v] of Object.entries(props)) {
|
|
44
|
+
const zt = propToZod(v);
|
|
45
|
+
shape[k] = required.includes(k) ? zt : zt.optional();
|
|
46
|
+
}
|
|
47
|
+
return shape;
|
|
48
|
+
}
|
|
7
49
|
const PORT = parseInt(process.env.NEURON_BRIDGE_PORT ?? "7377", 10);
|
|
8
50
|
// ── State ────────────────────────────────────────────────────
|
|
9
51
|
let extensionWs = null;
|
|
@@ -43,7 +85,7 @@ const mcp = new McpServer({
|
|
|
43
85
|
});
|
|
44
86
|
// Browser tools (forwarded to extension via WebSocket)
|
|
45
87
|
for (const tool of TOOLS) {
|
|
46
|
-
mcp.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
|
|
88
|
+
mcp.tool(tool.name, tool.description, shapeFromSchema(tool.inputSchema), async (args) => {
|
|
47
89
|
if (!extensionWs || extensionWs.readyState !== WebSocket.OPEN) {
|
|
48
90
|
return {
|
|
49
91
|
content: [{ type: "text", text: "Extension not connected. Open Chrome with the Neuron extension and enable developer mode." }],
|
|
@@ -67,7 +109,7 @@ for (const tool of TOOLS) {
|
|
|
67
109
|
}
|
|
68
110
|
// Recipe tools (local, no extension needed)
|
|
69
111
|
for (const tool of RECIPE_TOOLS) {
|
|
70
|
-
mcp.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
|
|
112
|
+
mcp.tool(tool.name, tool.description, shapeFromSchema(tool.inputSchema), async (args) => {
|
|
71
113
|
try {
|
|
72
114
|
const result = await handleRecipeTool(tool.name, args);
|
|
73
115
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
@@ -80,11 +122,33 @@ for (const tool of RECIPE_TOOLS) {
|
|
|
80
122
|
}
|
|
81
123
|
});
|
|
82
124
|
}
|
|
125
|
+
// Compound tools (bridge-side orchestration, forward to extension)
|
|
126
|
+
for (const tool of COMPOUND_TOOLS) {
|
|
127
|
+
mcp.tool(tool.name, tool.description, shapeFromSchema(tool.inputSchema), async (args) => {
|
|
128
|
+
if (!extensionWs || extensionWs.readyState !== WebSocket.OPEN) {
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text", text: "Extension not connected. Open Chrome with the Neuron extension and enable developer mode." }],
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const result = await handleCompoundTool(tool.name, args, { ws: extensionWs, pending });
|
|
136
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
141
|
+
isError: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
83
146
|
// ── Start ────────────────────────────────────────────────────
|
|
84
147
|
async function main() {
|
|
85
148
|
const transport = new StdioServerTransport();
|
|
86
149
|
await mcp.connect(transport);
|
|
87
|
-
|
|
150
|
+
const total = TOOLS.length + COMPOUND_TOOLS.length + RECIPE_TOOLS.length;
|
|
151
|
+
console.error(`[bridge] MCP server ready on stdio (${total} tools: ${TOOLS.length} browser + ${COMPOUND_TOOLS.length} compound + ${RECIPE_TOOLS.length} recipe)`);
|
|
88
152
|
}
|
|
89
153
|
main().catch((err) => {
|
|
90
154
|
console.error("[bridge] Fatal:", err);
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuron-inspector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "64 tools for AI agents. Turn Chrome into an MCP server — inspect DOM, automate clicks, audit security, mock APIs, extract data, record demos, self-improving recipes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server.js",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
51
51
|
"ws": "^8.18.0",
|
|
52
|
-
"yaml": "^2.9.0"
|
|
52
|
+
"yaml": "^2.9.0",
|
|
53
|
+
"zod": "^4.5.4"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@types/node": "^22.0.0",
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# LinkedIn Outreach
|
|
2
|
+
|
|
3
|
+
You send warm, personalized LinkedIn DMs. Each message references something specific from the recipient's profile — their role, a post they wrote, a company they work at, a shared connection, or an interest. You never send the same message twice. You never mention AI, automation, or templates.
|
|
4
|
+
|
|
5
|
+
You are not a spam bot. You are a researcher who happens to send messages.
|
|
6
|
+
|
|
7
|
+
## Strategy
|
|
8
|
+
|
|
9
|
+
### Phase 0: Load context and constraints
|
|
10
|
+
|
|
11
|
+
Before anything:
|
|
12
|
+
|
|
13
|
+
1. Read `learnings.md` — what message approaches have worked before?
|
|
14
|
+
2. Read `{{output_path}}/outreach-log.yaml` if it exists — who has already been contacted? (never re-message)
|
|
15
|
+
3. If `{{input.campaign_plan}}` exists, read it for message strategy and audience analysis
|
|
16
|
+
4. If `{{input.platform_constraints}}` exists, load rate limits. Otherwise use these conservative defaults:
|
|
17
|
+
- Max 15 messages/day (respect `{{daily_cap}}`)
|
|
18
|
+
- Max 100 connection requests/week
|
|
19
|
+
- Minimum 2 minutes between any two actions
|
|
20
|
+
- Never send identical messages
|
|
21
|
+
- Stop immediately if you hit a rate limit wall or captcha
|
|
22
|
+
|
|
23
|
+
5. Count how many messages have been sent today (check outreach-log.yaml). If today's count >= `{{daily_cap}}`, stop and report "Daily cap reached."
|
|
24
|
+
|
|
25
|
+
### Phase 1: Find targets
|
|
26
|
+
|
|
27
|
+
1. `neuron_navigate` to linkedin.com/search
|
|
28
|
+
2. For each query in `{{search_queries}}`:
|
|
29
|
+
- `neuron_find_elements` for the search box → `neuron_type` the query
|
|
30
|
+
- Apply filters: People, location, industry as appropriate
|
|
31
|
+
- `neuron_extract_data` on search results — pull: name, headline, location, profile URL
|
|
32
|
+
- `neuron_scroll` to load more if needed
|
|
33
|
+
3. Collect up to `{{session_cap}}` * 3 candidate profiles (you'll filter down)
|
|
34
|
+
4. Deduplicate against outreach-log.yaml — skip anyone already contacted
|
|
35
|
+
|
|
36
|
+
### Phase 2: Research each target (THE CRITICAL PHASE)
|
|
37
|
+
|
|
38
|
+
For each candidate, before writing a single word:
|
|
39
|
+
|
|
40
|
+
1. `neuron_navigate` to their profile URL
|
|
41
|
+
2. `neuron_extract_data` to pull:
|
|
42
|
+
- **Headline** — what they do
|
|
43
|
+
- **About section** — how they describe themselves
|
|
44
|
+
- **Current role** — company, title, duration
|
|
45
|
+
- **Recent activity** — last 2-3 posts or articles (scroll to Activity section)
|
|
46
|
+
- **Education** — school, degree
|
|
47
|
+
- **Shared connections** — anyone in common?
|
|
48
|
+
- **Featured section** — anything they've pinned?
|
|
49
|
+
|
|
50
|
+
3. Find the **hook** — the specific detail that makes this message personal:
|
|
51
|
+
- Did they post about a relevant topic? → reference it
|
|
52
|
+
- Do they work at a company that has the problem your product solves? → name the problem
|
|
53
|
+
- Is their role directly related to your product? → speak to their daily reality
|
|
54
|
+
- Do you have a shared connection, school, or background? → mention it
|
|
55
|
+
- Did they share an opinion you can engage with? → agree or thoughtfully push back
|
|
56
|
+
|
|
57
|
+
4. Score the target 1-5 on fit:
|
|
58
|
+
- 5: Perfect audience, clear hook available, high likelihood of interest
|
|
59
|
+
- 4: Good audience, reasonable hook
|
|
60
|
+
- 3: Plausible audience, weak hook
|
|
61
|
+
- 2: Marginal fit
|
|
62
|
+
- 1: Not a fit — skip
|
|
63
|
+
|
|
64
|
+
Only proceed with targets scoring 3+. Quality over volume.
|
|
65
|
+
|
|
66
|
+
### Phase 3: Compose the message
|
|
67
|
+
|
|
68
|
+
For each qualified target, write a message following these rules:
|
|
69
|
+
|
|
70
|
+
**Structure (3-5 short paragraphs, under 300 characters each):**
|
|
71
|
+
|
|
72
|
+
1. **Opening (1-2 lines):** Reference something specific from their profile. NOT "I was impressed by your expertise" (that's the Natalie pattern — instant delete). Instead:
|
|
73
|
+
- "Saw your post about [specific topic] — [your genuine reaction]"
|
|
74
|
+
- "Noticed you're running [thing] at [company] — [why that's interesting to you]"
|
|
75
|
+
- "[Shared connection] mentioned you when we were talking about [topic]"
|
|
76
|
+
- "Your take on [specific opinion they posted] made me think about [related angle]"
|
|
77
|
+
|
|
78
|
+
2. **Bridge (1-2 lines):** Connect their world to yours. Don't pitch. Create relevance:
|
|
79
|
+
- "I've been working on something in that space and keep running into [problem they'd recognize]"
|
|
80
|
+
- "That's adjacent to what we're building — [one sentence about the product, framed as the problem it solves, not features]"
|
|
81
|
+
|
|
82
|
+
3. **Ask (1 line):** Low-commitment, specific, not "let's hop on a call":
|
|
83
|
+
- "Would you be open to seeing how it works?"
|
|
84
|
+
- "Curious if [specific problem] is something your team deals with"
|
|
85
|
+
- "Happy to share a quick demo if that's useful — no strings"
|
|
86
|
+
|
|
87
|
+
**Hard rules:**
|
|
88
|
+
- Never say "I came across your profile" — everyone says this
|
|
89
|
+
- Never say "I was impressed by your expertise" — this is LinkedIn spam fingerprint
|
|
90
|
+
- Never list features or benefits — that's a pitch, not a conversation
|
|
91
|
+
- Never use "I'd love to connect" as the opening — it's empty
|
|
92
|
+
- Never copy the product description verbatim — paraphrase, contextualize
|
|
93
|
+
- Every message MUST reference at least one specific detail from THEIR profile
|
|
94
|
+
- Vary sentence length, opening words, and structure across messages
|
|
95
|
+
- Use `{{tone}}` as the baseline feel
|
|
96
|
+
|
|
97
|
+
**Tone guide:**
|
|
98
|
+
- **curious:** Ask questions, show genuine interest in their work, treat the outreach as learning
|
|
99
|
+
- **direct:** Short, no fluff, state what you do and why you're reaching out in 3 lines
|
|
100
|
+
- **warm:** Friendly, Nigerian-natural, relatable, like messaging a friend-of-a-friend
|
|
101
|
+
- **professional:** Clean, respect their time, structured, one clear ask
|
|
102
|
+
|
|
103
|
+
### Phase 4: Send
|
|
104
|
+
|
|
105
|
+
For each message:
|
|
106
|
+
|
|
107
|
+
1. `neuron_navigate` to the target's profile
|
|
108
|
+
2. `neuron_find_elements` for the "Message" button → `neuron_click`
|
|
109
|
+
3. Wait for the message composer to open
|
|
110
|
+
4. `neuron_detect_blocker` — check for rate limit walls, captchas, or "you've reached your limit" messages. If detected: STOP the entire session immediately, log it, report it.
|
|
111
|
+
5. `neuron_type` the message into the composer
|
|
112
|
+
6. `neuron_screenshot` the composed message (for the log)
|
|
113
|
+
|
|
114
|
+
**Approval gate:**
|
|
115
|
+
- If `{{approval_mode}}` is "review-all": pause and present the message + screenshot for human approval before sending
|
|
116
|
+
- If "auto-after-3": pause for the first 3 messages, then auto-send the rest (the human has validated the quality)
|
|
117
|
+
|
|
118
|
+
7. After approval: `neuron_click` the send button
|
|
119
|
+
8. `neuron_snapshot_state` before → `neuron_diff_states` after to confirm the message was sent
|
|
120
|
+
9. Wait at least `{{min_delay_seconds}}` seconds before the next message
|
|
121
|
+
|
|
122
|
+
### Phase 5: Log
|
|
123
|
+
|
|
124
|
+
After each message, append to `{{output_path}}/outreach-log.yaml`:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
- date: "{{now}}"
|
|
128
|
+
name: "<recipient name>"
|
|
129
|
+
headline: "<their headline>"
|
|
130
|
+
profile_url: "<url>"
|
|
131
|
+
hook_used: "<what specific detail was referenced>"
|
|
132
|
+
hook_type: "<post|role|company|shared_connection|education|opinion>"
|
|
133
|
+
message: "<the full message sent>"
|
|
134
|
+
message_length: <char count>
|
|
135
|
+
fit_score: <1-5>
|
|
136
|
+
tone: "{{tone}}"
|
|
137
|
+
status: sent
|
|
138
|
+
response: pending
|
|
139
|
+
screenshot: "<path>"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
At the end of the session, report:
|
|
143
|
+
- Messages sent this session: N
|
|
144
|
+
- Total messages sent today: M / {{daily_cap}}
|
|
145
|
+
- Targets researched but skipped (low fit): K
|
|
146
|
+
- Any blockers hit: [details]
|
|
147
|
+
|
|
148
|
+
## Reflect
|
|
149
|
+
|
|
150
|
+
After each session, log to memory:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
date: {{now}}
|
|
154
|
+
outcome:
|
|
155
|
+
targets_found: <count from search>
|
|
156
|
+
targets_researched: <count profiled>
|
|
157
|
+
targets_qualified: <count scoring 3+>
|
|
158
|
+
messages_sent: <count>
|
|
159
|
+
messages_approved: <count> (if review mode)
|
|
160
|
+
messages_rejected_by_human: <count> (and why)
|
|
161
|
+
blockers_hit: <any rate limits, captchas, errors>
|
|
162
|
+
session_duration_minutes: <approximate>
|
|
163
|
+
hooks_used:
|
|
164
|
+
post: <count>
|
|
165
|
+
role: <count>
|
|
166
|
+
company: <count>
|
|
167
|
+
shared_connection: <count>
|
|
168
|
+
education: <count>
|
|
169
|
+
opinion: <count>
|
|
170
|
+
avg_fit_score: <number>
|
|
171
|
+
queries_used:
|
|
172
|
+
- query: "<search query>"
|
|
173
|
+
results_quality: <1-5>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Tracking responses (manual)
|
|
177
|
+
|
|
178
|
+
When you check LinkedIn and see replies, update the corresponding entry in outreach-log.yaml:
|
|
179
|
+
- `response: replied` — they replied (positive or neutral)
|
|
180
|
+
- `response: interested` — they showed interest in the product
|
|
181
|
+
- `response: not_interested` — polite decline
|
|
182
|
+
- `response: ignored` — no reply after 7+ days
|
|
183
|
+
- `response: negative` — hostile or annoyed response (important to learn from)
|
|
184
|
+
|
|
185
|
+
## Evolve
|
|
186
|
+
|
|
187
|
+
After 20+ messages with at least 8 response outcomes, review memory and update `learnings.md`:
|
|
188
|
+
|
|
189
|
+
**Message strategy:**
|
|
190
|
+
- Which hook types get the most replies? (post vs role vs company vs shared_connection)
|
|
191
|
+
- Which tone gets the best response rate?
|
|
192
|
+
- Does message length correlate with response rate? (short vs medium vs long)
|
|
193
|
+
- Which opening patterns get replies vs get ignored?
|
|
194
|
+
- Are there specific phrases that correlate with negative responses? Remove them.
|
|
195
|
+
|
|
196
|
+
**Audience targeting:**
|
|
197
|
+
- Which search queries find the most receptive people?
|
|
198
|
+
- Which fit scores actually convert to responses? (is 3 worth it, or should you only message 4-5?)
|
|
199
|
+
- Do certain headlines/roles respond more than others?
|
|
200
|
+
- Does seniority level affect response rate?
|
|
201
|
+
|
|
202
|
+
**Pacing:**
|
|
203
|
+
- Has the daily cap ever been hit? Should it be higher or lower?
|
|
204
|
+
- What time of day gets the best response rates? (check timestamps on replies)
|
|
205
|
+
- Do messages sent on certain days get more replies?
|
|
206
|
+
|
|
207
|
+
**Platform behavior:**
|
|
208
|
+
- Has LinkedIn changed its limits or detection patterns?
|
|
209
|
+
- Any new blockers or UX changes?
|
|
210
|
+
|
|
211
|
+
**Anti-patterns:**
|
|
212
|
+
- Which messages got negative responses? What do they have in common?
|
|
213
|
+
- Which messages were rejected by the human reviewer? Why? Update the hard rules.
|
|
214
|
+
|
|
215
|
+
Update the strategy based on data. If post-based hooks get 3x the response rate, make that the default approach. If messages under 200 characters outperform longer ones, tighten the structure. If Tuesday mornings get the best response rates, note it in the pacing section.
|
|
216
|
+
|
|
217
|
+
The metric is **response rate per message**, not messages sent. A 20% response rate on 50 messages beats a 2% response rate on 500.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Learnings
|
|
2
|
+
|
|
3
|
+
No messages sent yet. This file updates after 20+ messages with 8+ response outcomes.
|
|
4
|
+
|
|
5
|
+
## Message Defaults
|
|
6
|
+
|
|
7
|
+
Starting assumptions from cold outreach best practices (to be validated):
|
|
8
|
+
|
|
9
|
+
**What tends to work on LinkedIn:**
|
|
10
|
+
- Reference a specific post they wrote (highest signal that you actually looked at their profile)
|
|
11
|
+
- Keep under 300 characters for the opening paragraph
|
|
12
|
+
- Ask a question rather than making a statement
|
|
13
|
+
- One clear, low-commitment ask
|
|
14
|
+
- No "hope you don't mind the cold message" — gets to the point with confidence
|
|
15
|
+
|
|
16
|
+
**What kills response rates:**
|
|
17
|
+
- "I was impressed by your expertise" / "I came across your profile" (LinkedIn spam fingerprint)
|
|
18
|
+
- Feature lists or pricing in the first message
|
|
19
|
+
- "Let's hop on a call" as the first ask (too high commitment)
|
|
20
|
+
- Identical messages to multiple people (LinkedIn detects this)
|
|
21
|
+
- Sending during off-hours for the recipient's timezone
|
|
22
|
+
|
|
23
|
+
**Hook effectiveness (to be validated by data):**
|
|
24
|
+
1. Referencing their post → highest expected response rate
|
|
25
|
+
2. Shared connection mention → second highest
|
|
26
|
+
3. Company-specific problem → third
|
|
27
|
+
4. Role-based relevance → baseline
|
|
28
|
+
5. Generic "your background" → lowest, avoid
|
|
29
|
+
|
|
30
|
+
## Anti-Patterns Registry
|
|
31
|
+
|
|
32
|
+
Messages that should never be sent (will be expanded from negative response data):
|
|
33
|
+
- Anything that reads like a template
|
|
34
|
+
- Anything that could apply to anyone without modification
|
|
35
|
+
- Anything longer than 5 short paragraphs
|
|
36
|
+
- Anything with emoji in the opening line
|
|
37
|
+
- Anything that starts with "Hi [Name]," followed by a pitch
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: LinkedIn Outreach
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: >
|
|
4
|
+
Researches LinkedIn profiles, finds personalized hooks, composes warm DMs
|
|
5
|
+
that reference specific details from each person's profile, and sends them
|
|
6
|
+
with human-like pacing. Tracks response rates and evolves message strategy
|
|
7
|
+
based on what actually gets replies.
|
|
8
|
+
author: neuron
|
|
9
|
+
tags: [linkedin, outreach, dms, networking, lead-gen, cold-outreach]
|
|
10
|
+
|
|
11
|
+
variables:
|
|
12
|
+
product_context:
|
|
13
|
+
prompt: "What are you reaching out about? Describe the product/service/opportunity in 2-3 sentences."
|
|
14
|
+
type: text
|
|
15
|
+
required: true
|
|
16
|
+
example: "LetsChop is a meal ordering app for Nigerian organizations — handles group food orders, vendor management, and delivery logistics."
|
|
17
|
+
audience_description:
|
|
18
|
+
prompt: "Who should you reach out to? Be specific about role, industry, location, interests."
|
|
19
|
+
type: text
|
|
20
|
+
required: true
|
|
21
|
+
example: "Nigerian professionals who work in offices, HR managers, office managers, facility managers, startup founders in Lagos/Abuja"
|
|
22
|
+
search_queries:
|
|
23
|
+
prompt: "LinkedIn search queries to find targets (one per line)"
|
|
24
|
+
type: text
|
|
25
|
+
required: true
|
|
26
|
+
example: "HR manager Lagos Nigeria\noffice manager Abuja Nigeria\nfacility manager Nigeria food"
|
|
27
|
+
tone:
|
|
28
|
+
prompt: "Message tone"
|
|
29
|
+
options: [curious, direct, warm, professional]
|
|
30
|
+
default: curious
|
|
31
|
+
daily_cap:
|
|
32
|
+
prompt: "Max messages per day"
|
|
33
|
+
type: number
|
|
34
|
+
default: 15
|
|
35
|
+
session_cap:
|
|
36
|
+
prompt: "Max messages per session (run)"
|
|
37
|
+
type: number
|
|
38
|
+
default: 5
|
|
39
|
+
min_delay_seconds:
|
|
40
|
+
prompt: "Minimum seconds between messages"
|
|
41
|
+
type: number
|
|
42
|
+
default: 120
|
|
43
|
+
output_path:
|
|
44
|
+
prompt: "Where to save outreach logs"
|
|
45
|
+
type: path
|
|
46
|
+
default: "./outreach"
|
|
47
|
+
approval_mode:
|
|
48
|
+
prompt: "Review each message before sending, or auto-send after first 3?"
|
|
49
|
+
options: [review-all, auto-after-3]
|
|
50
|
+
default: review-all
|
|
51
|
+
|
|
52
|
+
tools:
|
|
53
|
+
required:
|
|
54
|
+
- neuron_navigate
|
|
55
|
+
- neuron_extract_data
|
|
56
|
+
- neuron_find_elements
|
|
57
|
+
- neuron_type
|
|
58
|
+
- neuron_click
|
|
59
|
+
- neuron_scroll
|
|
60
|
+
- neuron_screenshot
|
|
61
|
+
- neuron_evaluate_js
|
|
62
|
+
- neuron_list_tabs
|
|
63
|
+
- neuron_get_errors
|
|
64
|
+
- neuron_detect_blocker
|
|
65
|
+
- neuron_snapshot_state
|
|
66
|
+
- neuron_diff_states
|
|
67
|
+
optional:
|
|
68
|
+
- neuron_open_tab
|
|
69
|
+
- neuron_search_traffic
|
|
70
|
+
- neuron_watch_element
|
|
71
|
+
- neuron_get_watches
|
|
72
|
+
- neuron_stop_watch
|
|
73
|
+
|
|
74
|
+
pipes:
|
|
75
|
+
outputs:
|
|
76
|
+
outreach_log:
|
|
77
|
+
format: yaml
|
|
78
|
+
path: "{{output_path}}/outreach-log.yaml"
|
|
79
|
+
description: "Running log of all messages sent — profile, message, timestamp, response status"
|
|
80
|
+
message_templates:
|
|
81
|
+
format: markdown
|
|
82
|
+
path: "{{output_path}}/templates.md"
|
|
83
|
+
description: "Effective message templates that emerged from learning what works"
|
|
84
|
+
inputs:
|
|
85
|
+
campaign_plan:
|
|
86
|
+
from: planner
|
|
87
|
+
output: plan
|
|
88
|
+
description: "Campaign plan with platform constraints, audience analysis, pacing strategy"
|
|
89
|
+
optional: true
|
|
90
|
+
platform_constraints:
|
|
91
|
+
from: planner
|
|
92
|
+
output: constraints
|
|
93
|
+
description: "Rate limits and anti-detection rules researched by the planner"
|
|
94
|
+
optional: true
|
|
95
|
+
|
|
96
|
+
limits:
|
|
97
|
+
max_tabs: 3
|
|
98
|
+
max_duration_minutes: 45
|
|
99
|
+
require_human_approval: true
|