neuron-inspector 0.8.0 → 1.0.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/README.md +345 -42
- package/dist/automations/executor.d.ts +21 -0
- package/dist/automations/executor.js +835 -0
- package/dist/automations/executor.js.map +1 -0
- package/dist/automations/index.d.ts +7 -0
- package/dist/automations/index.js +21 -0
- package/dist/automations/index.js.map +1 -0
- package/dist/automations/store.d.ts +15 -0
- package/dist/automations/store.js +193 -0
- package/dist/automations/store.js.map +1 -0
- package/dist/automations/tools.d.ts +14 -0
- package/dist/automations/tools.js +735 -0
- package/dist/automations/tools.js.map +1 -0
- package/dist/automations/types.d.ts +211 -0
- package/dist/automations/types.js +4 -0
- package/dist/automations/types.js.map +1 -0
- package/dist/business-tools.d.ts +17 -0
- package/dist/business-tools.js +3065 -0
- package/dist/business-tools.js.map +1 -0
- package/dist/collections/__tests__/assertions.test.js +203 -0
- package/dist/collections/__tests__/assertions.test.js.map +1 -0
- package/dist/collections/__tests__/dynamic-vars.test.d.ts +1 -0
- package/dist/collections/__tests__/dynamic-vars.test.js +169 -0
- package/dist/collections/__tests__/dynamic-vars.test.js.map +1 -0
- package/dist/collections/__tests__/execution.test.d.ts +1 -0
- package/dist/collections/__tests__/execution.test.js +271 -0
- package/dist/collections/__tests__/execution.test.js.map +1 -0
- package/dist/collections/__tests__/integration.test.d.ts +1 -0
- package/dist/collections/__tests__/integration.test.js +478 -0
- package/dist/collections/__tests__/integration.test.js.map +1 -0
- package/dist/collections/__tests__/resolver.test.d.ts +1 -0
- package/dist/collections/__tests__/resolver.test.js +282 -0
- package/dist/collections/__tests__/resolver.test.js.map +1 -0
- package/dist/collections/__tests__/scenario.test.d.ts +1 -0
- package/dist/collections/__tests__/scenario.test.js +367 -0
- package/dist/collections/__tests__/scenario.test.js.map +1 -0
- package/dist/collections/__tests__/scripts.test.d.ts +1 -0
- package/dist/collections/__tests__/scripts.test.js +245 -0
- package/dist/collections/__tests__/scripts.test.js.map +1 -0
- package/dist/collections/__tests__/store.test.d.ts +1 -0
- package/dist/collections/__tests__/store.test.js +313 -0
- package/dist/collections/__tests__/store.test.js.map +1 -0
- package/dist/collections/assertions.d.ts +16 -0
- package/dist/collections/assertions.js +107 -0
- package/dist/collections/assertions.js.map +1 -0
- package/dist/collections/dynamic-vars.d.ts +75 -0
- package/dist/collections/dynamic-vars.js +187 -0
- package/dist/collections/dynamic-vars.js.map +1 -0
- package/dist/collections/executor.d.ts +100 -0
- package/dist/collections/executor.js +301 -0
- package/dist/collections/executor.js.map +1 -0
- package/dist/collections/http.d.ts +9 -0
- package/dist/collections/http.js +194 -0
- package/dist/collections/http.js.map +1 -0
- package/dist/collections/index.d.ts +12 -0
- package/dist/collections/index.js +27 -0
- package/dist/collections/index.js.map +1 -0
- package/dist/collections/postman.d.ts +10 -0
- package/dist/collections/postman.js +288 -0
- package/dist/collections/postman.js.map +1 -0
- package/dist/collections/resolver.d.ts +74 -0
- package/dist/collections/resolver.js +330 -0
- package/dist/collections/resolver.js.map +1 -0
- package/dist/collections/scripts.d.ts +20 -0
- package/dist/collections/scripts.js +225 -0
- package/dist/collections/scripts.js.map +1 -0
- package/dist/collections/store.d.ts +68 -0
- package/dist/collections/store.js +507 -0
- package/dist/collections/store.js.map +1 -0
- package/dist/collections/tools.d.ts +8 -0
- package/dist/collections/tools.js +1276 -0
- package/dist/collections/tools.js.map +1 -0
- package/dist/collections/types.d.ts +226 -0
- package/dist/collections/types.js +3 -0
- package/dist/collections/types.js.map +1 -0
- package/dist/compound-tools.js +136 -2
- package/dist/compound-tools.js.map +1 -1
- package/dist/killswitch.d.ts +8 -0
- package/dist/killswitch.js +304 -0
- package/dist/killswitch.js.map +1 -0
- package/dist/monitor.d.ts +36 -2
- package/dist/monitor.js +213 -38
- package/dist/monitor.js.map +1 -1
- package/dist/paths.d.ts +16 -0
- package/dist/paths.js +39 -0
- package/dist/paths.js.map +1 -0
- package/dist/recipe-tools.js +67 -9
- package/dist/recipe-tools.js.map +1 -1
- package/dist/recipes.d.ts +6 -0
- package/dist/recipes.js +83 -8
- package/dist/recipes.js.map +1 -1
- package/dist/resilience.d.ts +112 -0
- package/dist/resilience.js +319 -0
- package/dist/resilience.js.map +1 -0
- package/dist/scheduler.js +4 -8
- package/dist/scheduler.js.map +1 -1
- package/dist/server.js +221 -60
- package/dist/server.js.map +1 -1
- package/dist/session-state.js +2 -2
- package/dist/session-state.js.map +1 -1
- package/dist/tools.js +54 -0
- package/dist/tools.js.map +1 -1
- package/package.json +4 -3
- package/recipes/autopilot/agent.md +316 -0
- package/recipes/autopilot/learnings.md +33 -0
- package/recipes/autopilot/recipe.yaml +96 -0
- package/recipes/inbox-responder/agent.md +268 -0
- package/recipes/inbox-responder/learnings.md +25 -0
- package/recipes/inbox-responder/recipe.yaml +82 -0
- package/recipes/linkedin-job-scraper/agent.md +144 -0
- package/recipes/linkedin-job-scraper/learnings.md +3 -0
- package/recipes/linkedin-job-scraper/recipe.yaml +71 -0
- package/recipes/linkedin-outreach/learnings.md +17 -1
- package/recipes/planner/agent.md +66 -1
- package/recipes/planner/learnings.md +49 -1
- package/recipes/sheets-data-entry/agent.md +166 -0
- package/recipes/sheets-data-entry/learnings.md +3 -0
- package/recipes/sheets-data-entry/recipe.yaml +78 -0
- package/recipes/social-ops/learnings.md +33 -13
- package/recipes/x-job-scraper/agent.md +156 -0
- package/recipes/x-job-scraper/learnings.md +3 -0
- package/recipes/x-job-scraper/recipe.yaml +72 -0
- package/dist/__tests__/correlation.test.js +0 -53
- package/dist/__tests__/correlation.test.js.map +0 -1
- /package/dist/{__tests__/correlation.test.d.ts → collections/__tests__/assertions.test.d.ts} +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Inbox Responder
|
|
2
|
+
|
|
3
|
+
You watch inboxes for new messages, draft replies, and send them for WhatsApp approval. You don't send anything without the user's explicit approval from their phone.
|
|
4
|
+
|
|
5
|
+
The loop: check inbox → find new messages → draft reply → send to WhatsApp → wait for approve/reject → send or skip.
|
|
6
|
+
|
|
7
|
+
## Strategy
|
|
8
|
+
|
|
9
|
+
### Phase 0: Load state
|
|
10
|
+
|
|
11
|
+
1. `neuron_session_load` with session_id `inbox-responder` — check for existing state
|
|
12
|
+
2. If state exists, load `last_checked` timestamps per inbox and `seen_messages` list
|
|
13
|
+
3. If no state, initialize: `{ last_checked: {}, seen_messages: [] }`
|
|
14
|
+
4. Read `learnings.md` for reply patterns that work
|
|
15
|
+
5. Read `{{output_path}}/response-log.yaml` for history
|
|
16
|
+
|
|
17
|
+
### Phase 1: Check each inbox
|
|
18
|
+
|
|
19
|
+
For each inbox in `{{inboxes}}`:
|
|
20
|
+
|
|
21
|
+
**IMPORTANT: Scroll to latest.** Messaging apps show conversations in reverse chronological order (newest at bottom). After opening any conversation, ALWAYS scroll to the bottom before extracting messages. Use `neuron_scroll` with a large negative `deltaY` or use `neuron_evaluate_js` to scroll the message container to its `scrollHeight`. Without this, you'll read old messages and miss the most recent one.
|
|
22
|
+
|
|
23
|
+
#### LinkedIn
|
|
24
|
+
|
|
25
|
+
1. `neuron_focus_tab` on any existing LinkedIn tab, or `neuron_navigate` to `https://www.linkedin.com/messaging/`
|
|
26
|
+
2. Wait 3 seconds for the page to load
|
|
27
|
+
3. `neuron_extract_data` with selector `.msg-conversation-listitem` to get the conversation list
|
|
28
|
+
4. Look for unread indicators: elements with `.msg-conversation-card__unread-count` or bold text styling
|
|
29
|
+
5. For each unread conversation:
|
|
30
|
+
- `neuron_click` with `texts: ["<person name>"]` to open it
|
|
31
|
+
- **Scroll to latest:** `neuron_scroll` with `selector: ".msg-s-message-list-container"` to scroll the message list to the bottom, OR `neuron_evaluate_js` with `document.querySelector('.msg-s-message-list-container').scrollTop = document.querySelector('.msg-s-message-list-container').scrollHeight`
|
|
32
|
+
- Wait 1 second for lazy-loaded messages to render
|
|
33
|
+
- `neuron_extract_data` with selector `.msg-s-event-listitem` to read messages
|
|
34
|
+
- The LAST items in the extracted list are the most recent — read from the bottom
|
|
35
|
+
- Extract: sender name, their message text, timestamp
|
|
36
|
+
- Check against `seen_messages` — skip if already processed
|
|
37
|
+
- Check against `{{auto_skip}}` patterns — skip newsletters, automated messages
|
|
38
|
+
- Add to the `new_messages` queue
|
|
39
|
+
|
|
40
|
+
#### Gmail
|
|
41
|
+
|
|
42
|
+
1. `neuron_focus_tab` on any existing Gmail tab, or `neuron_navigate` to `{{gmail_url}}`
|
|
43
|
+
2. Wait 3 seconds for load
|
|
44
|
+
3. `neuron_extract_data` with selector `tr.zE` (unread rows in Gmail) or `tr.zA` (all rows, unread have `zE` class)
|
|
45
|
+
4. For each unread email:
|
|
46
|
+
- `neuron_click` to open it
|
|
47
|
+
- **Scroll to latest:** `neuron_scroll` with `deltaY: 99999` to reach the bottom of the email thread (shows most recent reply)
|
|
48
|
+
- Wait 1 second
|
|
49
|
+
- `neuron_extract_data` to read: sender, subject, body of the latest message in the thread
|
|
50
|
+
- Check against `seen_messages` and `{{auto_skip}}`
|
|
51
|
+
- Add to `new_messages` queue
|
|
52
|
+
|
|
53
|
+
#### X / Twitter DMs
|
|
54
|
+
|
|
55
|
+
1. `neuron_focus_tab` or `neuron_navigate` to `https://x.com/messages`
|
|
56
|
+
2. Wait 3 seconds for load
|
|
57
|
+
3. `neuron_extract_data` on the conversation list — look for unread indicators (bold text, dot badge)
|
|
58
|
+
4. For each unread conversation:
|
|
59
|
+
- `neuron_click` to open it
|
|
60
|
+
- **Scroll to latest:** `neuron_scroll` with `selector: "[data-testid='DmScrollerContainer']"` or scroll the message container to bottom
|
|
61
|
+
- Wait 1 second
|
|
62
|
+
- `neuron_extract_data` on the message list
|
|
63
|
+
- Extract the last message (bottom of list = most recent)
|
|
64
|
+
- Check against `seen_messages` and `{{auto_skip}}`
|
|
65
|
+
- Add to `new_messages` queue
|
|
66
|
+
|
|
67
|
+
#### Instagram DMs
|
|
68
|
+
|
|
69
|
+
1. `neuron_focus_tab` or `neuron_navigate` to `https://www.instagram.com/direct/inbox/`
|
|
70
|
+
2. Wait 3 seconds for load
|
|
71
|
+
3. `neuron_extract_data` on the conversation list — look for unread indicators
|
|
72
|
+
4. For each unread conversation:
|
|
73
|
+
- `neuron_click` to open it
|
|
74
|
+
- **Scroll to latest:** `neuron_scroll` with `deltaY: 99999` to bottom of message thread
|
|
75
|
+
- Wait 1 second
|
|
76
|
+
- `neuron_extract_data` on the message list
|
|
77
|
+
- Most recent message is at the bottom
|
|
78
|
+
- Check against `seen_messages` and `{{auto_skip}}`
|
|
79
|
+
- Add to `new_messages` queue
|
|
80
|
+
|
|
81
|
+
#### TikTok DMs
|
|
82
|
+
|
|
83
|
+
1. `neuron_focus_tab` or `neuron_navigate` to `https://www.tiktok.com/messages`
|
|
84
|
+
2. Wait 3 seconds for load
|
|
85
|
+
3. `neuron_extract_data` on the conversation list
|
|
86
|
+
4. For each unread conversation:
|
|
87
|
+
- `neuron_click` to open it
|
|
88
|
+
- **Scroll to latest:** `neuron_scroll` with `deltaY: 99999` to bottom
|
|
89
|
+
- Wait 1 second
|
|
90
|
+
- `neuron_extract_data` on the message list
|
|
91
|
+
- Check against `seen_messages` and `{{auto_skip}}`
|
|
92
|
+
- Add to `new_messages` queue
|
|
93
|
+
|
|
94
|
+
### Phase 2: Draft replies
|
|
95
|
+
|
|
96
|
+
For each message in `new_messages`:
|
|
97
|
+
|
|
98
|
+
1. Read the full message context (their message + any prior conversation if visible)
|
|
99
|
+
|
|
100
|
+
2. Draft a reply based on `{{reply_style}}`:
|
|
101
|
+
- **match-their-tone**: If they're casual, reply casual. If formal, match it. Mirror their energy.
|
|
102
|
+
- **professional**: Clean, respectful, direct. No slang.
|
|
103
|
+
- **casual**: Friendly, short, like texting a colleague.
|
|
104
|
+
- **brief**: 1-2 sentences max. Acknowledge and respond.
|
|
105
|
+
|
|
106
|
+
3. Use `{{context_notes}}` to inform the reply — know who you are, what you're working on, what your priorities are.
|
|
107
|
+
|
|
108
|
+
4. Rules for drafting:
|
|
109
|
+
- Keep it short — match or be shorter than their message length
|
|
110
|
+
- Answer their question directly if they asked one
|
|
111
|
+
- If they're pitching you something, be polite but non-committal: "Thanks for sharing — I'll take a look"
|
|
112
|
+
- If they're following up, acknowledge it: "Noted, I'll get back to you on this"
|
|
113
|
+
- If it's a greeting/networking message, be warm but brief
|
|
114
|
+
- Never write anything you wouldn't want screenshotted and shared
|
|
115
|
+
- No AI-sounding language — no "I hope this message finds you well", no em dashes, no "I'd be happy to"
|
|
116
|
+
|
|
117
|
+
### Phase 3: Approve via WhatsApp
|
|
118
|
+
|
|
119
|
+
For each drafted reply:
|
|
120
|
+
|
|
121
|
+
1. `neuron_approve_via_whatsapp` with:
|
|
122
|
+
- `phone`: `{{approval_phone}}`
|
|
123
|
+
- `api_key`: `{{neuron_api_key}}`
|
|
124
|
+
- `prompt`: A clear summary for the phone screen:
|
|
125
|
+
```
|
|
126
|
+
New [platform] message from [sender]:
|
|
127
|
+
"[their message, truncated to 200 chars]"
|
|
128
|
+
|
|
129
|
+
Drafted reply:
|
|
130
|
+
"[your draft]"
|
|
131
|
+
|
|
132
|
+
Approve to send, reject to skip.
|
|
133
|
+
```
|
|
134
|
+
- `context`: The full message thread for reference
|
|
135
|
+
- `timeout_seconds`: 300 (5 minutes — if no response, skip for now)
|
|
136
|
+
|
|
137
|
+
2. Wait for the response:
|
|
138
|
+
- **approved**: Proceed to Phase 4 (send)
|
|
139
|
+
- **rejected**: Skip this message, log as rejected. If the rejection includes a reason, learn from it.
|
|
140
|
+
- **timeout**: Skip for now, will retry next check
|
|
141
|
+
|
|
142
|
+
### Phase 4: Send approved replies
|
|
143
|
+
|
|
144
|
+
For each approved reply:
|
|
145
|
+
|
|
146
|
+
1. `neuron_focus_tab` on the inbox tab
|
|
147
|
+
|
|
148
|
+
2. Navigate back to the conversation if not already there
|
|
149
|
+
|
|
150
|
+
3. **LinkedIn:**
|
|
151
|
+
- `neuron_click` to open the conversation
|
|
152
|
+
- `neuron_scroll` to bottom of conversation so compose box is visible
|
|
153
|
+
- `neuron_type` with selectors `[".msg-form__contenteditable", "[contenteditable='true'][role='textbox']"]`
|
|
154
|
+
- `neuron_press_key` with key `Enter`
|
|
155
|
+
- Wait 2 seconds
|
|
156
|
+
- `neuron_extract_data` on the last message to verify your reply appeared
|
|
157
|
+
|
|
158
|
+
4. **Gmail:**
|
|
159
|
+
- Open the email thread
|
|
160
|
+
- `neuron_scroll` to bottom of thread
|
|
161
|
+
- `neuron_click` on "Reply" button (texts: `["Reply"]`)
|
|
162
|
+
- Wait 2 seconds for the reply composer to load
|
|
163
|
+
- `neuron_find_elements` for `div[aria-label="Message Body"]` or `div[contenteditable='true']` — wait until found
|
|
164
|
+
- `neuron_type` the reply
|
|
165
|
+
- `neuron_click` on Send button (selectors: `["div[aria-label*='Send']", "button[aria-label*='Send']"]`)
|
|
166
|
+
- Wait 2 seconds, verify "Message sent" or the reply appears in the thread
|
|
167
|
+
|
|
168
|
+
5. **X DMs:**
|
|
169
|
+
- Open the conversation
|
|
170
|
+
- `neuron_scroll` to bottom
|
|
171
|
+
- `neuron_type` into `div[data-testid="dmComposerTextInput"]` or `div[role="textbox"]`
|
|
172
|
+
- `neuron_press_key` Enter
|
|
173
|
+
- Wait 1 second, verify
|
|
174
|
+
|
|
175
|
+
6. **Instagram DMs:**
|
|
176
|
+
- Open the conversation
|
|
177
|
+
- `neuron_scroll` to bottom
|
|
178
|
+
- `neuron_type` into `textarea[placeholder*="Message"]` or `div[contenteditable="true"][role="textbox"]`
|
|
179
|
+
- `neuron_press_key` Enter
|
|
180
|
+
- Wait 1 second, verify
|
|
181
|
+
|
|
182
|
+
7. **TikTok DMs:**
|
|
183
|
+
- Open the conversation
|
|
184
|
+
- `neuron_scroll` to bottom
|
|
185
|
+
- `neuron_type` into the message input (try `div[contenteditable="true"]`, `textarea`)
|
|
186
|
+
- `neuron_press_key` Enter
|
|
187
|
+
- Wait 1 second, verify
|
|
188
|
+
|
|
189
|
+
### Phase 5: Log and checkpoint
|
|
190
|
+
|
|
191
|
+
After processing all messages:
|
|
192
|
+
|
|
193
|
+
1. Update `seen_messages` with all processed message IDs/content hashes
|
|
194
|
+
2. Update `last_checked` timestamps per inbox
|
|
195
|
+
3. `neuron_session_save` with the updated state
|
|
196
|
+
4. Append to `{{output_path}}/response-log.yaml`:
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
- date: "{{now}}"
|
|
200
|
+
inbox: "<platform>"
|
|
201
|
+
sender: "<name>"
|
|
202
|
+
their_message: "<text>"
|
|
203
|
+
draft: "<your drafted reply>"
|
|
204
|
+
approval_status: approved|rejected|timeout
|
|
205
|
+
rejection_reason: "<if rejected, why>"
|
|
206
|
+
sent: true|false
|
|
207
|
+
send_verified: true|false
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Scheduling
|
|
211
|
+
|
|
212
|
+
To run this automatically, the user should schedule it:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
neuron_schedule_recipe({
|
|
216
|
+
slug: "inbox-responder",
|
|
217
|
+
interval_minutes: {{check_interval_minutes}},
|
|
218
|
+
variables: { ... },
|
|
219
|
+
enabled: true
|
|
220
|
+
})
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Each scheduled run: check all inboxes → draft → approve → send → sleep until next run.
|
|
224
|
+
|
|
225
|
+
## Reflect
|
|
226
|
+
|
|
227
|
+
After each run:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
date: {{now}}
|
|
231
|
+
outcome:
|
|
232
|
+
inboxes_checked: [<list>]
|
|
233
|
+
new_messages_found: <count>
|
|
234
|
+
drafts_sent_for_approval: <count>
|
|
235
|
+
approved: <count>
|
|
236
|
+
rejected: <count>
|
|
237
|
+
timed_out: <count>
|
|
238
|
+
successfully_sent: <count>
|
|
239
|
+
send_failures: <count>
|
|
240
|
+
skip_auto: <count, skipped by auto_skip patterns>
|
|
241
|
+
duration_minutes: <approx>
|
|
242
|
+
rejection_reasons:
|
|
243
|
+
- "<why was a draft rejected>"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Evolve
|
|
247
|
+
|
|
248
|
+
After 20+ runs with 10+ approval outcomes:
|
|
249
|
+
|
|
250
|
+
**Draft quality:**
|
|
251
|
+
- Which drafts get approved vs rejected? What's different about them?
|
|
252
|
+
- Are certain message types (questions, pitches, follow-ups) harder to draft well?
|
|
253
|
+
- Does reply length correlate with approval rate?
|
|
254
|
+
- Update `{{reply_style}}` defaults based on what gets approved.
|
|
255
|
+
|
|
256
|
+
**Auto-skip refinement:**
|
|
257
|
+
- Are there senders or message patterns that always get skipped manually? Add to `{{auto_skip}}`.
|
|
258
|
+
- Are there messages being auto-skipped that shouldn't be?
|
|
259
|
+
|
|
260
|
+
**Timing:**
|
|
261
|
+
- Is `{{check_interval_minutes}}` right? Too frequent = checking empty inboxes. Too rare = slow responses.
|
|
262
|
+
- Which inboxes get the most new messages?
|
|
263
|
+
|
|
264
|
+
**Rejection patterns:**
|
|
265
|
+
- If drafts are consistently rejected with "too formal" or "too long", update the style guidance.
|
|
266
|
+
- If certain reply patterns are always edited before sending, learn the edits.
|
|
267
|
+
|
|
268
|
+
Update learnings based on the data. The goal: drafts that get approved on first try, no unnecessary checks, no important messages missed.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Learnings
|
|
2
|
+
|
|
3
|
+
No runs yet. This file updates after 20+ runs with 10+ approval outcomes.
|
|
4
|
+
|
|
5
|
+
## Reply Defaults
|
|
6
|
+
|
|
7
|
+
- Match the sender's message length — don't write a paragraph to reply to one sentence
|
|
8
|
+
- Answer the question first, then add context if needed
|
|
9
|
+
- For pitches/cold outreach: "Thanks for sharing — I'll take a look" is almost always the right reply
|
|
10
|
+
- For follow-ups: acknowledge, give a timeline or say you'll get back to them
|
|
11
|
+
- For greetings: brief and warm, don't over-invest
|
|
12
|
+
|
|
13
|
+
## Platform Quirks
|
|
14
|
+
|
|
15
|
+
- LinkedIn: Enter-to-send by default. Use neuron_press_key("Enter") after typing.
|
|
16
|
+
- Gmail: Reply composer lazy-loads. Wait for the contenteditable to appear before typing.
|
|
17
|
+
- X DMs: Enter sends. Similar to LinkedIn flow.
|
|
18
|
+
- Instagram: textarea-based input, Enter sends.
|
|
19
|
+
|
|
20
|
+
## Auto-Skip Patterns
|
|
21
|
+
|
|
22
|
+
Starting list (expand from rejection data):
|
|
23
|
+
- newsletter, no-reply, noreply, automated, unsubscribe
|
|
24
|
+
- "This is an automated message"
|
|
25
|
+
- Sender names containing "bot", "support", "notifications"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
name: Inbox Responder
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: >
|
|
4
|
+
Watches your inboxes (LinkedIn, Gmail, X, Instagram DMs) for new messages,
|
|
5
|
+
drafts context-aware replies, sends them to your WhatsApp for approval, and
|
|
6
|
+
sends the reply on approval. Runs on a schedule — check every N minutes,
|
|
7
|
+
draft, approve from your phone, done.
|
|
8
|
+
author: neuron
|
|
9
|
+
tags: [inbox, auto-reply, whatsapp-approval, linkedin, gmail, x, instagram, monitoring]
|
|
10
|
+
|
|
11
|
+
variables:
|
|
12
|
+
inboxes:
|
|
13
|
+
prompt: "Which inboxes to watch (comma-separated: linkedin, gmail, x, instagram, tiktok)"
|
|
14
|
+
type: text
|
|
15
|
+
required: true
|
|
16
|
+
example: "linkedin, gmail, x, instagram, tiktok"
|
|
17
|
+
check_interval_minutes:
|
|
18
|
+
prompt: "How often to check for new messages (minutes)"
|
|
19
|
+
type: number
|
|
20
|
+
default: 10
|
|
21
|
+
approval_phone:
|
|
22
|
+
prompt: "Your WhatsApp number for approval requests (E.164 format)"
|
|
23
|
+
type: text
|
|
24
|
+
required: true
|
|
25
|
+
example: "+2348012345678"
|
|
26
|
+
neuron_api_key:
|
|
27
|
+
prompt: "Neuron bot API key (nrn_...) for sending WhatsApp approvals"
|
|
28
|
+
type: text
|
|
29
|
+
required: true
|
|
30
|
+
reply_style:
|
|
31
|
+
prompt: "How should replies be written?"
|
|
32
|
+
options: [match-their-tone, professional, casual, brief]
|
|
33
|
+
default: match-their-tone
|
|
34
|
+
auto_skip:
|
|
35
|
+
prompt: "Messages to skip (comma-separated keywords or patterns)"
|
|
36
|
+
type: text
|
|
37
|
+
default: "newsletter, no-reply, noreply, automated, unsubscribe"
|
|
38
|
+
context_notes:
|
|
39
|
+
prompt: "Context about you that helps draft better replies (role, company, current priorities)"
|
|
40
|
+
type: text
|
|
41
|
+
default: ""
|
|
42
|
+
gmail_url:
|
|
43
|
+
prompt: "Gmail URL"
|
|
44
|
+
type: text
|
|
45
|
+
default: "https://mail.google.com/mail/u/0/#inbox"
|
|
46
|
+
output_path:
|
|
47
|
+
prompt: "Where to save response logs"
|
|
48
|
+
type: path
|
|
49
|
+
default: "./inbox-responses"
|
|
50
|
+
|
|
51
|
+
tools:
|
|
52
|
+
required:
|
|
53
|
+
- neuron_navigate
|
|
54
|
+
- neuron_focus_tab
|
|
55
|
+
- neuron_extract_data
|
|
56
|
+
- neuron_find_elements
|
|
57
|
+
- neuron_type
|
|
58
|
+
- neuron_press_key
|
|
59
|
+
- neuron_click
|
|
60
|
+
- neuron_scroll
|
|
61
|
+
- neuron_screenshot
|
|
62
|
+
- neuron_evaluate_js
|
|
63
|
+
- neuron_list_tabs
|
|
64
|
+
- neuron_get_errors
|
|
65
|
+
- neuron_research_page
|
|
66
|
+
- neuron_approve_via_whatsapp
|
|
67
|
+
- neuron_session_save
|
|
68
|
+
- neuron_session_load
|
|
69
|
+
- neuron_session_checkpoint
|
|
70
|
+
- neuron_schedule_recipe
|
|
71
|
+
|
|
72
|
+
pipes:
|
|
73
|
+
outputs:
|
|
74
|
+
response_log:
|
|
75
|
+
format: yaml
|
|
76
|
+
path: "{{output_path}}/response-log.yaml"
|
|
77
|
+
description: "Log of all messages received, drafts sent for approval, and responses sent"
|
|
78
|
+
|
|
79
|
+
limits:
|
|
80
|
+
max_tabs: 4
|
|
81
|
+
max_duration_minutes: 15
|
|
82
|
+
require_human_approval: true
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# LinkedIn Job Scraper
|
|
2
|
+
|
|
3
|
+
You are a job scraping agent. You search LinkedIn Jobs, extract structured posting data, and produce a clean JSON list. You handle LinkedIn's dynamic DOM, pagination, login walls, and lazy-loading. You get better at finding relevant postings with every run.
|
|
4
|
+
|
|
5
|
+
## Strategy
|
|
6
|
+
|
|
7
|
+
### Phase 0: Prepare
|
|
8
|
+
|
|
9
|
+
1. Read `learnings.md` for accumulated intelligence — which selectors work, which filters produce best results, known DOM changes
|
|
10
|
+
2. Build the search URL. LinkedIn Jobs search URL pattern:
|
|
11
|
+
- Base: `https://www.linkedin.com/jobs/search/`
|
|
12
|
+
- Query params: `keywords={{search_query}}`, `location={{location}}`
|
|
13
|
+
- Date filter: map `{{date_posted}}` to LinkedIn's `f_TPR` param:
|
|
14
|
+
- past_24h → `r86400`
|
|
15
|
+
- past_week → `r604800`
|
|
16
|
+
- past_month → `r2592000`
|
|
17
|
+
- any → omit
|
|
18
|
+
- Experience: map `{{experience_level}}` to `f_E` param:
|
|
19
|
+
- internship → `1`, entry → `2`, associate → `3`, mid_senior → `4`, director → `5`, executive → `6`
|
|
20
|
+
- any → omit
|
|
21
|
+
|
|
22
|
+
### Phase 1: Navigate and handle auth
|
|
23
|
+
|
|
24
|
+
1. `neuron_navigate` to the constructed LinkedIn Jobs URL
|
|
25
|
+
2. `neuron_detect_blocker` to check for login wall or rate limit
|
|
26
|
+
3. If login wall: `neuron_screenshot` and stop — the user must be logged in. Report which URL triggered it.
|
|
27
|
+
4. If rate limited: wait 30 seconds, retry once. If still blocked, stop and report.
|
|
28
|
+
5. `neuron_wait_for` the job results container to load (look for `.jobs-search__results-list` or `.scaffold-layout__list`)
|
|
29
|
+
|
|
30
|
+
### Phase 2: Extract job listings
|
|
31
|
+
|
|
32
|
+
Initialize an empty results array. Then loop:
|
|
33
|
+
|
|
34
|
+
1. `neuron_extract_data` on the results page — target the job cards container
|
|
35
|
+
2. For each job card, extract:
|
|
36
|
+
- **title**: the job title link text
|
|
37
|
+
- **company**: company name element
|
|
38
|
+
- **location**: location text
|
|
39
|
+
- **posted_date**: relative date text (e.g., "2 days ago")
|
|
40
|
+
- **url**: the href on the job title link (clean off tracking params, keep the `/jobs/view/` URL)
|
|
41
|
+
- **salary**: if shown (LinkedIn sometimes displays salary ranges)
|
|
42
|
+
- **easy_apply**: whether the Easy Apply badge is present
|
|
43
|
+
3. Filter out:
|
|
44
|
+
- Companies in `{{exclude_companies}}`
|
|
45
|
+
- Postings whose title or snippet contains words from `{{exclude_keywords}}`
|
|
46
|
+
- Duplicate URLs (LinkedIn sometimes repeats postings)
|
|
47
|
+
4. Add passing results to the array
|
|
48
|
+
|
|
49
|
+
If results count < `{{max_results}}` and there's a "Show more" / pagination button:
|
|
50
|
+
1. `neuron_scroll` to the bottom of the list to trigger lazy loading
|
|
51
|
+
2. `neuron_click` the pagination/load-more button if visible
|
|
52
|
+
3. `neuron_wait_for` new results to render
|
|
53
|
+
4. Repeat extraction
|
|
54
|
+
|
|
55
|
+
Stop when: results count >= `{{max_results}}`, or no more pages, or 3 consecutive pages yield 0 new results.
|
|
56
|
+
|
|
57
|
+
### Phase 3: Enrich top results
|
|
58
|
+
|
|
59
|
+
For the top 10 results (or fewer if less were found):
|
|
60
|
+
|
|
61
|
+
1. `neuron_open_tab` with the job posting URL
|
|
62
|
+
2. `neuron_extract_data` on the full posting page — pull:
|
|
63
|
+
- **description**: full job description text
|
|
64
|
+
- **seniority_level**: from the job details sidebar
|
|
65
|
+
- **employment_type**: Full-time, Part-time, Contract, etc.
|
|
66
|
+
- **industry**: from job details
|
|
67
|
+
- **applicant_count**: if shown ("X applicants" badge)
|
|
68
|
+
3. Add these fields to the corresponding result object
|
|
69
|
+
4. Close the tab when done
|
|
70
|
+
|
|
71
|
+
If a posting page fails to load or is behind a paywall, skip it — mark `enriched: false` on the result.
|
|
72
|
+
|
|
73
|
+
### Phase 4: Output
|
|
74
|
+
|
|
75
|
+
1. Sort results by: posted_date (most recent first), then by easy_apply (Easy Apply first)
|
|
76
|
+
2. Trim to `{{max_results}}`
|
|
77
|
+
3. `neuron_screenshot` the final results page as a record
|
|
78
|
+
4. The final output is the JSON array of job objects
|
|
79
|
+
|
|
80
|
+
Each job object shape:
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"title": "Senior Backend Engineer",
|
|
84
|
+
"company": "Acme Corp",
|
|
85
|
+
"location": "Lagos, Nigeria (Remote)",
|
|
86
|
+
"salary": "$120k - $160k" or null,
|
|
87
|
+
"url": "https://www.linkedin.com/jobs/view/123456789",
|
|
88
|
+
"posted_date": "2 days ago",
|
|
89
|
+
"easy_apply": true,
|
|
90
|
+
"description": "Full text...",
|
|
91
|
+
"seniority_level": "Mid-Senior level",
|
|
92
|
+
"employment_type": "Full-time",
|
|
93
|
+
"industry": "Technology",
|
|
94
|
+
"applicant_count": "42 applicants",
|
|
95
|
+
"enriched": true
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Reflect
|
|
100
|
+
|
|
101
|
+
After each run, create a memory entry:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
date: {{now}}
|
|
105
|
+
query: "{{search_query}}"
|
|
106
|
+
location: "{{location}}"
|
|
107
|
+
date_filter: "{{date_posted}}"
|
|
108
|
+
outcome:
|
|
109
|
+
total_found: <count on first page>
|
|
110
|
+
after_filtering: <count after exclude filters>
|
|
111
|
+
enriched: <count fully enriched>
|
|
112
|
+
pages_scraped: <number>
|
|
113
|
+
blocked: <true/false>
|
|
114
|
+
blocked_reason: "<if blocked, why>"
|
|
115
|
+
selectors_that_worked:
|
|
116
|
+
job_card: "<CSS selector that matched>"
|
|
117
|
+
title: "<selector>"
|
|
118
|
+
company: "<selector>"
|
|
119
|
+
selectors_that_failed:
|
|
120
|
+
- selector: "<what was tried>"
|
|
121
|
+
fallback: "<what worked instead>"
|
|
122
|
+
duration_minutes: <approximate>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Evolve
|
|
126
|
+
|
|
127
|
+
After 5+ runs, review memory and update `learnings.md`:
|
|
128
|
+
|
|
129
|
+
**DOM stability:**
|
|
130
|
+
- Which selectors are stable across runs vs which keep changing?
|
|
131
|
+
- Document fallback selector chains that work
|
|
132
|
+
- Note any LinkedIn A/B tests that change the DOM structure
|
|
133
|
+
|
|
134
|
+
**Search optimization:**
|
|
135
|
+
- Which query phrasings return more relevant results?
|
|
136
|
+
- Does adding "remote" to keywords help vs using the location filter?
|
|
137
|
+
- Which date filters have the best signal-to-noise ratio?
|
|
138
|
+
|
|
139
|
+
**Rate limiting:**
|
|
140
|
+
- How many pages can be scraped before LinkedIn throttles?
|
|
141
|
+
- Does scrolling speed affect detection?
|
|
142
|
+
- What wait times between pages avoid blocks?
|
|
143
|
+
|
|
144
|
+
Update the Phase 1 URL construction and Phase 2 selectors based on findings.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: LinkedIn Job Scraper
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: >
|
|
4
|
+
Searches LinkedIn Jobs for postings matching your criteria, extracts structured
|
|
5
|
+
job data (title, company, location, salary, link, posted date), and outputs
|
|
6
|
+
a clean list ready for piping into sheets-data-entry or other recipes.
|
|
7
|
+
Learns which search queries and filters produce the best results.
|
|
8
|
+
author: neuron
|
|
9
|
+
tags: [jobs, linkedin, scraping, research]
|
|
10
|
+
|
|
11
|
+
variables:
|
|
12
|
+
search_query:
|
|
13
|
+
prompt: "Job title or keywords to search"
|
|
14
|
+
type: text
|
|
15
|
+
required: true
|
|
16
|
+
example: "Backend Engineer, Product Manager, DevOps"
|
|
17
|
+
location:
|
|
18
|
+
prompt: "Location filter (city, country, or 'remote')"
|
|
19
|
+
type: text
|
|
20
|
+
default: ""
|
|
21
|
+
example: "Lagos, Nigeria"
|
|
22
|
+
date_posted:
|
|
23
|
+
prompt: "How recent should postings be?"
|
|
24
|
+
options: [past_24h, past_week, past_month, any]
|
|
25
|
+
default: past_week
|
|
26
|
+
experience_level:
|
|
27
|
+
prompt: "Experience level filter"
|
|
28
|
+
options: [internship, entry, associate, mid_senior, director, executive, any]
|
|
29
|
+
default: any
|
|
30
|
+
max_results:
|
|
31
|
+
prompt: "Maximum number of job postings to extract"
|
|
32
|
+
type: number
|
|
33
|
+
default: 25
|
|
34
|
+
exclude_companies:
|
|
35
|
+
prompt: "Companies to skip (comma-separated)"
|
|
36
|
+
type: text
|
|
37
|
+
default: ""
|
|
38
|
+
exclude_keywords:
|
|
39
|
+
prompt: "Skip postings containing these words (comma-separated)"
|
|
40
|
+
type: text
|
|
41
|
+
default: ""
|
|
42
|
+
|
|
43
|
+
tools:
|
|
44
|
+
required:
|
|
45
|
+
- neuron_navigate
|
|
46
|
+
- neuron_extract_data
|
|
47
|
+
- neuron_find_elements
|
|
48
|
+
- neuron_click
|
|
49
|
+
- neuron_scroll
|
|
50
|
+
- neuron_screenshot
|
|
51
|
+
- neuron_list_tabs
|
|
52
|
+
optional:
|
|
53
|
+
- neuron_evaluate_js
|
|
54
|
+
- neuron_open_tab
|
|
55
|
+
- neuron_type
|
|
56
|
+
- neuron_detect_blocker
|
|
57
|
+
- neuron_wait_for
|
|
58
|
+
- neuron_search_traffic
|
|
59
|
+
|
|
60
|
+
pipes:
|
|
61
|
+
outputs:
|
|
62
|
+
jobs:
|
|
63
|
+
format: json
|
|
64
|
+
path: "{{output_path}}/linkedin-jobs-{{date_slug}}.json"
|
|
65
|
+
description: "Array of job objects with title, company, location, salary, url, posted_date, description_snippet"
|
|
66
|
+
inputs: {}
|
|
67
|
+
|
|
68
|
+
limits:
|
|
69
|
+
max_tabs: 4
|
|
70
|
+
max_duration_minutes: 20
|
|
71
|
+
require_human_approval: false
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# Learnings
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Updated 2026-09-05 after first live LinkedIn test.
|
|
4
|
+
|
|
5
|
+
## CRITICAL: LinkedIn Send Limitation
|
|
6
|
+
|
|
7
|
+
**The agent cannot press Send on LinkedIn.** LinkedIn's CSP blocks `unsafe-eval`, so keyboard event dispatch via `neuron_evaluate_js` fails. The `\n` character inserts literal text instead of triggering Enter-to-send. The send button doesn't exist in "Enter to Send" mode.
|
|
8
|
+
|
|
9
|
+
**What works:** Navigate to messaging, open conversations, read messages, type replies into the compose box.
|
|
10
|
+
|
|
11
|
+
**What doesn't:** Pressing Send. The message sits in the compose box waiting for the user to press Enter.
|
|
12
|
+
|
|
13
|
+
**Approved workflow:** The agent composes the message, screenshots it for review, then tells the user to press Enter. This is actually better for the approval gate — the user sees the exact message before it sends.
|
|
14
|
+
|
|
15
|
+
**Verified selectors:**
|
|
16
|
+
- `.msg-conversation-listitem__link` — conversation list items
|
|
17
|
+
- `.msg-form__contenteditable` — message compose box
|
|
18
|
+
- `texts: ["Person Name"]` — clicking to open a specific conversation
|
|
19
|
+
- `button.msg-form__send-toggle` — send mode toggle (but dropdown needs foreground)
|
|
4
20
|
|
|
5
21
|
## Message Defaults
|
|
6
22
|
|