bereach-openclaw 0.2.13 → 0.2.15
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/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/skills/bereach/SKILL.md +36 -13
- package/skills/bereach/sdk-reference.md +296 -2
- package/skills/bereach/sub/lead-magnet.md +91 -41
- package/src/tools/definitions.ts +2 -2
- package/src/tools/index.ts +8 -1
- package/scripts/test-register.ts +0 -38
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "bereach",
|
|
3
3
|
"name": "BeReach",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.15",
|
|
5
5
|
"description": "LinkedIn outreach automation — 33 tools, auto-reply commands",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"BEREACH_API_KEY": {
|
|
11
11
|
"type": "string",
|
|
12
12
|
"minLength": 1,
|
|
13
|
+
"pattern": "^brc_",
|
|
13
14
|
"description": "BeReach API token (starts with brc_)"
|
|
14
15
|
}
|
|
15
16
|
}
|
package/package.json
CHANGED
package/skills/bereach/SKILL.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bereach
|
|
3
3
|
description: "Automate LinkedIn outreach via BeReach (berea.ch). Use when: prospecting, engaging posts, scraping engagement, searching LinkedIn, managing inbox, running campaigns, managing invitations. Requires BEREACH_API_KEY."
|
|
4
|
-
lastUpdatedAt:
|
|
4
|
+
lastUpdatedAt: 1772673508
|
|
5
|
+
metadata: { "openclaw": { "requires": { "env": ["BEREACH_API_KEY"] }, "primaryEnv": "BEREACH_API_KEY" } }
|
|
5
6
|
---
|
|
6
7
|
|
|
7
8
|
# BeReach — LinkedIn Outreach Automation
|
|
@@ -16,9 +17,16 @@ Automate LinkedIn prospection and engagement via BeReach.
|
|
|
16
17
|
|
|
17
18
|
| Sub-skill | Keywords | URL | lastUpdatedAt |
|
|
18
19
|
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------- |
|
|
19
|
-
| Lead Magnet | lead magnet, comment to DM, resource delivery, post giveaway, auto-accept invitations,
|
|
20
|
+
| Lead Magnet | lead magnet, comment to DM, resource delivery, post giveaway, auto-accept invitations, scheduled run, recap, campaign stats, pause | sub/lead-magnet.md | 1772673508 |
|
|
20
21
|
| OpenClaw Optimization | openclaw, model, opus, sonnet, haiku, config, SOUL.md, heartbeat, prompt caching, AI cost reduction, /model | openclaw-optimization.md | 1772619338 |
|
|
21
|
-
| SDK Reference | sdk, method, parameter, signature, reference, api, script | sdk-reference.md |
|
|
22
|
+
| SDK Reference | sdk, method, parameter, signature, reference, api, script | sdk-reference.md | 1772672714 |
|
|
23
|
+
|
|
24
|
+
### Supporting files
|
|
25
|
+
|
|
26
|
+
Load these when the user's request matches the keywords above:
|
|
27
|
+
|
|
28
|
+
- For lead magnet campaigns (comment-to-DM, resource delivery, scheduled runs), see [Lead Magnet](sub/lead-magnet.md)
|
|
29
|
+
- For SDK method signatures and parameters when writing scripts, see [SDK Reference](sdk-reference.md)
|
|
22
30
|
|
|
23
31
|
## Installation
|
|
24
32
|
|
|
@@ -28,7 +36,7 @@ Tools are already configured by the plugin — no manual setup needed.
|
|
|
28
36
|
|
|
29
37
|
### SDK (script generation)
|
|
30
38
|
|
|
31
|
-
The TypeScript SDK provides typed methods for all BeReach operations. Use it when generating scripts (
|
|
39
|
+
The TypeScript SDK provides typed methods for all BeReach operations. Use it when generating scripts (scheduled automations, batch jobs).
|
|
32
40
|
|
|
33
41
|
```bash
|
|
34
42
|
npm add bereach
|
|
@@ -47,6 +55,14 @@ const client = new Bereach({ token });
|
|
|
47
55
|
|
|
48
56
|
The SDK auto-reads `BEREACH_API_KEY` from the environment. NEVER hardcode tokens in scripts.
|
|
49
57
|
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
1. `GET SKILL.md` → compare frontmatter `lastUpdatedAt` with local copy
|
|
61
|
+
2. If remote > local: re-download main skill, replace local copy
|
|
62
|
+
3. Compare sub-skill `lastUpdatedAt` values — re-fetch any that changed
|
|
63
|
+
4. Log: "BeReach skill updated" or "BeReach skill is up to date"
|
|
64
|
+
5. Never skip this check
|
|
65
|
+
|
|
50
66
|
## Onboarding
|
|
51
67
|
|
|
52
68
|
1. **Token** — read `BEREACH_API_KEY` from environment with no default/fallback value. If not set, stop immediately.
|
|
@@ -63,6 +79,7 @@ The SDK auto-reads `BEREACH_API_KEY` from the environment. NEVER hardcode tokens
|
|
|
63
79
|
|
|
64
80
|
### Script mode (SDK)
|
|
65
81
|
|
|
82
|
+
- Lead magnet scripts use the SDK. Tools are for interactive agent use only.
|
|
66
83
|
- Import from the `bereach` SDK exclusively. If a method doesn't exist, the operation doesn't exist.
|
|
67
84
|
- Scripts MUST be TypeScript (`.ts`). Run with `npx tsx script.ts`.
|
|
68
85
|
- Run `npx tsc --noEmit` before executing to catch type errors.
|
|
@@ -114,24 +131,30 @@ These are technical constraints BeReach requires. Everything else, adapt as need
|
|
|
114
131
|
|
|
115
132
|
Each workflow is detailed in its sub-skill. Load the relevant sub-skill when needed.
|
|
116
133
|
|
|
117
|
-
- **Lead Magnet** — deliver a resource to everyone who engages with a post (comments, likes, invitations). → Lead Magnet sub-skill
|
|
134
|
+
- **Lead Magnet** — deliver a resource to everyone who engages with a post (comments, likes, invitations). Multi-campaign config at `~/.bereach/lead-magnet.json`. → Lead Magnet sub-skill
|
|
118
135
|
|
|
119
136
|
More workflows coming soon. You can build your own using the SDK methods and tools listed above.
|
|
120
137
|
|
|
121
138
|
### Cron
|
|
122
139
|
|
|
123
|
-
Crons are OpenClaw scheduled tasks. Create entries in `~/.openclaw/
|
|
140
|
+
Crons are OpenClaw scheduled tasks. Create entries in `~/.openclaw/cron/jobs.json` (via `openclaw cron add`).
|
|
141
|
+
|
|
142
|
+
**Lead Magnet** — 3 global crons. Config at `~/.openclaw/lead-magnet.json`. Each cron runs one script that loops over all enabled campaigns:
|
|
124
143
|
|
|
125
144
|
```json
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"skill": "bereach",
|
|
130
|
-
"sessionTarget": "spawn",
|
|
131
|
-
"prompt": "Run {workflow} script and report results"
|
|
132
|
-
}
|
|
145
|
+
{ "id": "lm-comments", "every": "1h", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run lm-comments.ts and report recap" }
|
|
146
|
+
{ "id": "lm-invitations", "every": "1h", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run lm-invitations.ts and report recap" }
|
|
147
|
+
{ "id": "lm-connections", "every": "1h", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run lm-connections.ts and report recap" }
|
|
133
148
|
```
|
|
134
149
|
|
|
135
150
|
Always use `"sessionTarget": "spawn"` for crons that execute scripts. This spawns a sub-agent in an isolated session — the script runs independently and pushes results back to the user when done, without blocking the main session. Never use `exec()` directly from a cron prompt; it blocks until the script finishes.
|
|
136
151
|
|
|
152
|
+
**Update check** — detect-only, never auto-upgrade:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{ "id": "bereach-update-check", "every": "1d", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Check if a new BeReach plugin version is available. Compare local version from package.json with `npm view bereach-openclaw version`. If newer: notify user once: 'BeReach vX.Y.Z is available (current: vA.B.C). Say upgrade bereach when ready.' Do NOT upgrade automatically." }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
When the user says "upgrade bereach": follow the upgrade workflow in `upgrade-openclaw-bereach.md` — save user state, uninstall, reinstall, recreate scripts from new skill, wait for user confirmation before launching crons.
|
|
159
|
+
|
|
137
160
|
Remove with `openclaw cron remove <id>`.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bereach-sdk-reference
|
|
3
3
|
description: "Complete SDK method reference — parameters, types, and descriptions for all BeReach operations."
|
|
4
|
-
lastUpdatedAt:
|
|
4
|
+
lastUpdatedAt: 1772672714
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# BeReach SDK Reference
|
|
8
8
|
|
|
9
9
|
> Auto-generated from OpenAPI spec. This is the single source of truth for SDK method signatures.
|
|
10
|
-
> Use `client.<resource>.<method>(params)` — see [BeReach Skill](
|
|
10
|
+
> Use `client.<resource>.<method>(params)` — see [BeReach Skill](SKILL.md) for behavioral rules.
|
|
11
11
|
|
|
12
12
|
## linkedinScrapers
|
|
13
13
|
|
|
@@ -21,6 +21,17 @@ Scrape LinkedIn post likes. Returns paginated list of profiles who liked a post.
|
|
|
21
21
|
- **start** (integer, min 0) — Pagination offset (multiples of 200).
|
|
22
22
|
- **count** (integer, 0-200) — Number of likes to fetch per page (0-200, default 200). Use count=0 for a free total-only check.
|
|
23
23
|
|
|
24
|
+
**Returns:**
|
|
25
|
+
|
|
26
|
+
- **profiles** (array)
|
|
27
|
+
- **count** (number)
|
|
28
|
+
- **total** (number)
|
|
29
|
+
- **start** (number)
|
|
30
|
+
- **hasMore** (boolean)
|
|
31
|
+
- **previousTotal** (number | null) — The total from your last call to this endpoint for the same post (null on first call). Compare with total to detect new likes without client-side tracking.
|
|
32
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
33
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
34
|
+
|
|
24
35
|
### collectComments
|
|
25
36
|
|
|
26
37
|
Scrape LinkedIn post comments. Returns paginated list of commenters with comment text and URNs.
|
|
@@ -32,6 +43,19 @@ Scrape LinkedIn post comments. Returns paginated list of commenters with comment
|
|
|
32
43
|
- **count** (integer, 0-100) — Number of comments to fetch per page (0-100, default 100). Use count=0 for a free total-only check.
|
|
33
44
|
- **campaignSlug** (string) — When provided, each profile includes actionsCompleted and knownDistance for campaign-aware scraping.
|
|
34
45
|
|
|
46
|
+
**Returns:**
|
|
47
|
+
|
|
48
|
+
- **profiles** (array)
|
|
49
|
+
- **count** (number)
|
|
50
|
+
- **total** (number)
|
|
51
|
+
- **start** (number)
|
|
52
|
+
- **hasMore** (boolean)
|
|
53
|
+
- **previousTotal** (number | null) — The total from your last call to this endpoint for the same post (null on first call). Compare with total to detect new comments without client-side tracking.
|
|
54
|
+
- **processedCount** (number, optional) — Number of returned profiles that already have a completed 'message' action in this campaign. Only present when campaignSlug is provided.
|
|
55
|
+
- **newCount** (number, optional) — Number of returned profiles that have NOT yet been messaged in this campaign. Only present when campaignSlug is provided.
|
|
56
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
57
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
58
|
+
|
|
35
59
|
### collectCommentReplies
|
|
36
60
|
|
|
37
61
|
Scrape replies to a LinkedIn comment. Returns paginated replies for a specific comment URN.
|
|
@@ -42,6 +66,18 @@ Scrape replies to a LinkedIn comment. Returns paginated replies for a specific c
|
|
|
42
66
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
43
67
|
- **count** (integer, 0-100) — Number of replies to fetch per page (0-100, default 100). Use count=0 for a free total-only check.
|
|
44
68
|
|
|
69
|
+
**Returns:**
|
|
70
|
+
|
|
71
|
+
- **replies** (array)
|
|
72
|
+
- **count** (number)
|
|
73
|
+
- **total** (number)
|
|
74
|
+
- **start** (number)
|
|
75
|
+
- **hasMore** (boolean)
|
|
76
|
+
- **parentCommentUrn** (string)
|
|
77
|
+
- **previousTotal** (number | null) — The total from your last call to this endpoint for the same comment (null on first call). Compare with total to detect new replies without client-side tracking.
|
|
78
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
79
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
80
|
+
|
|
45
81
|
### collectPosts
|
|
46
82
|
|
|
47
83
|
Scrape LinkedIn profile posts. Returns paginated list of posts from a profile.
|
|
@@ -53,6 +89,18 @@ Scrape LinkedIn profile posts. Returns paginated list of posts from a profile.
|
|
|
53
89
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
54
90
|
- **paginationToken** (string) — Pagination token from a previous response to fetch the next page.
|
|
55
91
|
|
|
92
|
+
**Returns:**
|
|
93
|
+
|
|
94
|
+
- **posts** (array)
|
|
95
|
+
- **count** (number)
|
|
96
|
+
- **total** (number)
|
|
97
|
+
- **start** (number)
|
|
98
|
+
- **hasMore** (boolean)
|
|
99
|
+
- **paginationToken** (string | null)
|
|
100
|
+
- **previousTotal** (number | null) — The total from your last call to this endpoint for the same profile (null on first call). Compare with total to detect new posts without client-side tracking.
|
|
101
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
102
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
103
|
+
|
|
56
104
|
### visitProfile
|
|
57
105
|
|
|
58
106
|
Visit LinkedIn profile and extract contact data. Distance-1 profiles cached 24h. No dedup — always executes.
|
|
@@ -63,6 +111,28 @@ Visit LinkedIn profile and extract contact data. Distance-1 profiles cached 24h.
|
|
|
63
111
|
- **campaignSlug** (string) — Optional campaign identifier for tracking only. No dedup — visit always executes.
|
|
64
112
|
- **includePosts** (boolean) — When true, fetches the last 5 posts from the profile. Defaults to false.
|
|
65
113
|
|
|
114
|
+
**Returns:**
|
|
115
|
+
|
|
116
|
+
- **firstName** (string)
|
|
117
|
+
- **lastName** (string)
|
|
118
|
+
- **headline** (string | null)
|
|
119
|
+
- **publicIdentifier** (string)
|
|
120
|
+
- **profileUrl** (string)
|
|
121
|
+
- **profileUrn** (string, optional) — LinkedIn profile URN (e.g. 'urn:li:fsd_profile:ACoAAA...'). Use this for matching against inbox participants.
|
|
122
|
+
- **imageUrl** (string | null)
|
|
123
|
+
- **email** (string | null, optional)
|
|
124
|
+
- **location** (string | null, optional)
|
|
125
|
+
- **company** (string | null, optional) — Current company name (from most recent position)
|
|
126
|
+
- **position** (string | null, optional) — Current job title (from most recent position)
|
|
127
|
+
- **memberDistance** (number | null, optional)
|
|
128
|
+
- **pendingConnection** (string)
|
|
129
|
+
- **positions** (array, optional) — Work experience positions
|
|
130
|
+
- **educations** (array, optional) — Education entries
|
|
131
|
+
- **lastPosts** (array, optional) — Last 5 posts from the profile (only present when includePosts is true)
|
|
132
|
+
- **cached** (boolean) — true if this result was served from cache (0 credits). Distance-1 profiles are cached for 24h.
|
|
133
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
134
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
135
|
+
|
|
66
136
|
### visitCompany
|
|
67
137
|
|
|
68
138
|
Visit LinkedIn company page and extract profile data including description, industry, employee count, headquarters, and more.
|
|
@@ -72,6 +142,34 @@ Visit LinkedIn company page and extract profile data including description, indu
|
|
|
72
142
|
- **companyUrl** (string, required) — LinkedIn company URL (e.g., 'https://www.linkedin.com/company/openai') or universal name (e.g., 'openai').
|
|
73
143
|
- **includeWorkplacePolicy** (boolean) — Include workplace policy data such as hybrid/remote status and benefits. Costs 1 extra credit.
|
|
74
144
|
|
|
145
|
+
**Returns:**
|
|
146
|
+
|
|
147
|
+
- **id** (string) — LinkedIn numeric company ID
|
|
148
|
+
- **universalName** (string) — Company URL slug
|
|
149
|
+
- **name** (string)
|
|
150
|
+
- **description** (string | null)
|
|
151
|
+
- **url** (string) — LinkedIn company page URL
|
|
152
|
+
- **websiteUrl** (string | null) — External website URL
|
|
153
|
+
- **industry** (string | null)
|
|
154
|
+
- **employeeCount** (number | null) — Exact employee count on LinkedIn
|
|
155
|
+
- **employeeCountRange** (object)
|
|
156
|
+
- **end** (number | null) — Employee count range
|
|
157
|
+
- **logoUrl** (string | null)
|
|
158
|
+
- **coverImageUrl** (string | null)
|
|
159
|
+
- **followerCount** (number | null)
|
|
160
|
+
- **specialities** (array)
|
|
161
|
+
- **tagline** (string | null)
|
|
162
|
+
- **isVerified** (boolean)
|
|
163
|
+
- **foundedOn** (object | null)
|
|
164
|
+
- **phone** (string | null)
|
|
165
|
+
- **callToAction** (object)
|
|
166
|
+
- **url** (string | null)
|
|
167
|
+
- **hashtags** (array)
|
|
168
|
+
- **affiliatedCompanies** (array) — Showcase / affiliated pages
|
|
169
|
+
- **similarCompanies** (array) — Similar companies suggested by LinkedIn
|
|
170
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
171
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
172
|
+
|
|
75
173
|
## linkedinSearch
|
|
76
174
|
|
|
77
175
|
### unifiedSearch
|
|
@@ -106,6 +204,13 @@ Unified LinkedIn Search — search posts, people, companies, or jobs with a sing
|
|
|
106
204
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
107
205
|
- **count** (integer, 1-50) — Results per page (default 10, max 50).
|
|
108
206
|
|
|
207
|
+
**Returns:**
|
|
208
|
+
|
|
209
|
+
- **items** (unknown)
|
|
210
|
+
- **hasMore** (boolean)
|
|
211
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
212
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
213
|
+
|
|
109
214
|
### searchPosts
|
|
110
215
|
|
|
111
216
|
Search LinkedIn posts by keywords with optional filters for date, content type, author industry, and author company.
|
|
@@ -122,6 +227,13 @@ Search LinkedIn posts by keywords with optional filters for date, content type,
|
|
|
122
227
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
123
228
|
- **count** (integer, 1-50) — Results per page (default 10, max 50).
|
|
124
229
|
|
|
230
|
+
**Returns:**
|
|
231
|
+
|
|
232
|
+
- **items** (unknown)
|
|
233
|
+
- **hasMore** (boolean)
|
|
234
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
235
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
236
|
+
|
|
125
237
|
### searchPeople
|
|
126
238
|
|
|
127
239
|
Search LinkedIn people by keywords, connection degree, name, title, location, industry, company, and school.
|
|
@@ -144,6 +256,13 @@ Search LinkedIn people by keywords, connection degree, name, title, location, in
|
|
|
144
256
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
145
257
|
- **count** (integer, 1-50) — Results per page (default 10, max 50).
|
|
146
258
|
|
|
259
|
+
**Returns:**
|
|
260
|
+
|
|
261
|
+
- **items** (unknown)
|
|
262
|
+
- **hasMore** (boolean)
|
|
263
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
264
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
265
|
+
|
|
147
266
|
### searchCompanies
|
|
148
267
|
|
|
149
268
|
Search LinkedIn companies by keywords, location, industry, and company size.
|
|
@@ -158,6 +277,13 @@ Search LinkedIn companies by keywords, location, industry, and company size.
|
|
|
158
277
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
159
278
|
- **count** (integer, 1-50) — Results per page (default 10, max 50).
|
|
160
279
|
|
|
280
|
+
**Returns:**
|
|
281
|
+
|
|
282
|
+
- **items** (unknown)
|
|
283
|
+
- **hasMore** (boolean)
|
|
284
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
285
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
286
|
+
|
|
161
287
|
### searchJobs
|
|
162
288
|
|
|
163
289
|
Search LinkedIn jobs by keywords, location, job type, experience level, and workplace type.
|
|
@@ -175,6 +301,13 @@ Search LinkedIn jobs by keywords, location, job type, experience level, and work
|
|
|
175
301
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
176
302
|
- **count** (integer, 1-50) — Results per page (default 10, max 50).
|
|
177
303
|
|
|
304
|
+
**Returns:**
|
|
305
|
+
|
|
306
|
+
- **items** (unknown)
|
|
307
|
+
- **hasMore** (boolean)
|
|
308
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
309
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
310
|
+
|
|
178
311
|
### searchByUrl
|
|
179
312
|
|
|
180
313
|
Search LinkedIn by URL. Automatically extracts category, keywords, and filters from a LinkedIn search URL.
|
|
@@ -185,6 +318,13 @@ Search LinkedIn by URL. Automatically extracts category, keywords, and filters f
|
|
|
185
318
|
- **start** (integer, min 0) — Override pagination offset.
|
|
186
319
|
- **count** (integer, 1-50) — Override results per page (default 10, max 50).
|
|
187
320
|
|
|
321
|
+
**Returns:**
|
|
322
|
+
|
|
323
|
+
- **items** (unknown)
|
|
324
|
+
- **hasMore** (boolean)
|
|
325
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
326
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
327
|
+
|
|
188
328
|
### resolveParameters
|
|
189
329
|
|
|
190
330
|
Resolve text to LinkedIn search parameter IDs (typeahead). Prerequisite for using location, industry, company, or school filters.
|
|
@@ -195,6 +335,13 @@ Resolve text to LinkedIn search parameter IDs (typeahead). Prerequisite for usin
|
|
|
195
335
|
- **keywords** (string, required) — Text to resolve into LinkedIn IDs.
|
|
196
336
|
- **limit** (integer, 1-50) — Max results (default 10, max 50).
|
|
197
337
|
|
|
338
|
+
**Returns:**
|
|
339
|
+
|
|
340
|
+
- **items** (array)
|
|
341
|
+
- **count** (number)
|
|
342
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
343
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
344
|
+
|
|
198
345
|
## linkedinActions
|
|
199
346
|
|
|
200
347
|
### connectProfile
|
|
@@ -206,6 +353,13 @@ Send LinkedIn connection request. Deduplicates by profile within a campaign.
|
|
|
206
353
|
- **profile** (string, required) — LinkedIn profile URL or profile URN.
|
|
207
354
|
- **campaignSlug** (string) — Campaign identifier for deduplication.
|
|
208
355
|
|
|
356
|
+
**Returns:**
|
|
357
|
+
|
|
358
|
+
- **message** (string)
|
|
359
|
+
- **duplicate** (boolean, optional) — True if a connection request was already successfully sent to this profile this week
|
|
360
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
361
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
362
|
+
|
|
209
363
|
### listInvitations
|
|
210
364
|
|
|
211
365
|
List received LinkedIn connection invitations. Returns pending invitations with IDs needed to accept them.
|
|
@@ -215,6 +369,15 @@ List received LinkedIn connection invitations. Returns pending invitations with
|
|
|
215
369
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
216
370
|
- **count** (integer, 1-100) — Number of invitations to return (default 10, max 100).
|
|
217
371
|
|
|
372
|
+
**Returns:**
|
|
373
|
+
|
|
374
|
+
- **invitations** (array)
|
|
375
|
+
- **total** (number) — Total number of pending invitations
|
|
376
|
+
- **start** (number)
|
|
377
|
+
- **count** (number)
|
|
378
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
379
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
380
|
+
|
|
218
381
|
### acceptInvitation
|
|
219
382
|
|
|
220
383
|
Accept a LinkedIn connection invitation. Requires invitationId and sharedSecret from the list invitations endpoint.
|
|
@@ -227,6 +390,12 @@ Accept a LinkedIn connection invitation. Requires invitationId and sharedSecret
|
|
|
227
390
|
- **firstName** (string) — Sender's first name. Recommended for reliability.
|
|
228
391
|
- **lastName** (string) — Sender's last name. Recommended for reliability.
|
|
229
392
|
|
|
393
|
+
**Returns:**
|
|
394
|
+
|
|
395
|
+
- **message** (string)
|
|
396
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
397
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
398
|
+
|
|
230
399
|
### sendMessage
|
|
231
400
|
|
|
232
401
|
Send LinkedIn message. Rate limited to 80 messages per day.
|
|
@@ -237,6 +406,13 @@ Send LinkedIn message. Rate limited to 80 messages per day.
|
|
|
237
406
|
- **message** (string, required) — Message content to send.
|
|
238
407
|
- **campaignSlug** (string) — Campaign identifier for deduplication.
|
|
239
408
|
|
|
409
|
+
**Returns:**
|
|
410
|
+
|
|
411
|
+
- **messageId** (string)
|
|
412
|
+
- **duplicate** (boolean, optional) — True if this action was already executed for the given campaignSlug + target.
|
|
413
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
414
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
415
|
+
|
|
240
416
|
### replyToComment
|
|
241
417
|
|
|
242
418
|
Reply to a LinkedIn comment. Use the commentUrn from the comments endpoint directly.
|
|
@@ -247,6 +423,13 @@ Reply to a LinkedIn comment. Use the commentUrn from the comments endpoint direc
|
|
|
247
423
|
- **message** (string, required) — Reply message text.
|
|
248
424
|
- **campaignSlug** (string) — Campaign identifier for deduplication.
|
|
249
425
|
|
|
426
|
+
**Returns:**
|
|
427
|
+
|
|
428
|
+
- **replyUrn** (string, optional) — URN of the created reply comment
|
|
429
|
+
- **duplicate** (boolean, optional) — True if this action was already executed for the given campaignSlug + target.
|
|
430
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
431
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
432
|
+
|
|
250
433
|
### likeComment
|
|
251
434
|
|
|
252
435
|
Like (react to) a LinkedIn comment. Use the commentUrn from the comments endpoint directly.
|
|
@@ -257,6 +440,13 @@ Like (react to) a LinkedIn comment. Use the commentUrn from the comments endpoin
|
|
|
257
440
|
- **reactionType** (string) — Reaction type (default: LIKE). Values: LIKE, LOVE, CELEBRATE, SUPPORT, FUNNY, INSIGHTFUL.
|
|
258
441
|
- **campaignSlug** (string) — Campaign identifier for deduplication.
|
|
259
442
|
|
|
443
|
+
**Returns:**
|
|
444
|
+
|
|
445
|
+
- **resourceKey** (string, optional) — Resource key of the created reaction
|
|
446
|
+
- **duplicate** (boolean, optional) — True if this action was already executed for the given campaignSlug + target.
|
|
447
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
448
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
449
|
+
|
|
260
450
|
### publishPost
|
|
261
451
|
|
|
262
452
|
Publish or schedule a LinkedIn post. Supports text, images, mentions, and visibility control.
|
|
@@ -271,6 +461,14 @@ Publish or schedule a LinkedIn post. Supports text, images, mentions, and visibi
|
|
|
271
461
|
- **mentions** (object[]) — Profile mentions with text positions.
|
|
272
462
|
- **campaignSlug** (string) — Campaign identifier for deduplication.
|
|
273
463
|
|
|
464
|
+
**Returns:**
|
|
465
|
+
|
|
466
|
+
- **shareUrn** (string)
|
|
467
|
+
- **activityUrn** (string | null) — Activity URN (only available for instant mode)
|
|
468
|
+
- **duplicate** (boolean, optional) — True if this action was already executed for the given campaignSlug + target.
|
|
469
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
470
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
471
|
+
|
|
274
472
|
## linkedinChat
|
|
275
473
|
|
|
276
474
|
### listConversations
|
|
@@ -281,6 +479,13 @@ List LinkedIn inbox conversations with participants, last message, and read stat
|
|
|
281
479
|
|
|
282
480
|
- **nextCursor** (string) — Pagination cursor from a previous response.
|
|
283
481
|
|
|
482
|
+
**Returns:**
|
|
483
|
+
|
|
484
|
+
- **conversations** (array)
|
|
485
|
+
- **nextCursor** (string | null) — Cursor for fetching next page
|
|
486
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
487
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
488
|
+
|
|
284
489
|
### searchConversations
|
|
285
490
|
|
|
286
491
|
Search LinkedIn inbox conversations by keyword. 0 credits.
|
|
@@ -290,6 +495,13 @@ Search LinkedIn inbox conversations by keyword. 0 credits.
|
|
|
290
495
|
- **keywords** (string, required) — Search keywords.
|
|
291
496
|
- **nextCursor** (string) — Pagination cursor from a previous response.
|
|
292
497
|
|
|
498
|
+
**Returns:**
|
|
499
|
+
|
|
500
|
+
- **conversations** (array)
|
|
501
|
+
- **nextCursor** (string | null) — Cursor for fetching next page
|
|
502
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
503
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
504
|
+
|
|
293
505
|
### findConversation
|
|
294
506
|
|
|
295
507
|
Find a conversation with a specific person. Direct O(1) lookup via LinkedIn's compose API. 0 credits.
|
|
@@ -299,6 +511,13 @@ Find a conversation with a specific person. Direct O(1) lookup via LinkedIn's co
|
|
|
299
511
|
- **profile** (string, required) — Profile URL or URN for direct conversation lookup.
|
|
300
512
|
- **includeMessages** (boolean) — If true, also return the conversation's recent messages (0 extra credits). Default: false.
|
|
301
513
|
|
|
514
|
+
**Returns:**
|
|
515
|
+
|
|
516
|
+
- **found** (boolean)
|
|
517
|
+
- **messages** (array | null)
|
|
518
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
519
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
520
|
+
|
|
302
521
|
### getMessages
|
|
303
522
|
|
|
304
523
|
Read messages from a LinkedIn conversation. 0 credits.
|
|
@@ -308,6 +527,13 @@ Read messages from a LinkedIn conversation. 0 credits.
|
|
|
308
527
|
- **conversationUrn** (string, required) — Full conversation URN as returned by list/search conversations.
|
|
309
528
|
- **deliveredAt** (integer) — Timestamp (ms) of the oldest message from previous page — pass this to load older messages.
|
|
310
529
|
|
|
530
|
+
**Returns:**
|
|
531
|
+
|
|
532
|
+
- **messages** (array)
|
|
533
|
+
- **prevCursor** (number | null) — deliveredAt timestamp (ms) of the oldest message — pass as 'deliveredAt' to load older messages. Null when no more messages.
|
|
534
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
535
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
536
|
+
|
|
311
537
|
## profile
|
|
312
538
|
|
|
313
539
|
### getLinkedInProfile
|
|
@@ -318,6 +544,11 @@ Get authenticated user's LinkedIn profile from the database. No LinkedIn API cal
|
|
|
318
544
|
|
|
319
545
|
No parameters.
|
|
320
546
|
|
|
547
|
+
**Returns:**
|
|
548
|
+
|
|
549
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
550
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
551
|
+
|
|
321
552
|
### refresh
|
|
322
553
|
|
|
323
554
|
Refresh authenticated user's LinkedIn profile by fetching latest data from LinkedIn.
|
|
@@ -326,6 +557,12 @@ Refresh authenticated user's LinkedIn profile by fetching latest data from Linke
|
|
|
326
557
|
|
|
327
558
|
No parameters.
|
|
328
559
|
|
|
560
|
+
**Returns:**
|
|
561
|
+
|
|
562
|
+
- **refreshed** (true)
|
|
563
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
564
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
565
|
+
|
|
329
566
|
### getPosts
|
|
330
567
|
|
|
331
568
|
Get authenticated user's LinkedIn posts.
|
|
@@ -336,6 +573,17 @@ Get authenticated user's LinkedIn posts.
|
|
|
336
573
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
337
574
|
- **paginationToken** (string) — Pagination token from a previous response.
|
|
338
575
|
|
|
576
|
+
**Returns:**
|
|
577
|
+
|
|
578
|
+
- **posts** (array)
|
|
579
|
+
- **count** (number)
|
|
580
|
+
- **total** (number)
|
|
581
|
+
- **start** (number)
|
|
582
|
+
- **hasMore** (boolean)
|
|
583
|
+
- **paginationToken** (string | null)
|
|
584
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
585
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
586
|
+
|
|
339
587
|
### getFollowers
|
|
340
588
|
|
|
341
589
|
Get authenticated user's LinkedIn followers.
|
|
@@ -345,6 +593,16 @@ Get authenticated user's LinkedIn followers.
|
|
|
345
593
|
- **start** (integer, min 0) — Pagination offset (default 0).
|
|
346
594
|
- **count** (integer, 1-50) — Number of followers to fetch per page (default 10, max 50).
|
|
347
595
|
|
|
596
|
+
**Returns:**
|
|
597
|
+
|
|
598
|
+
- **followers** (array)
|
|
599
|
+
- **count** (number)
|
|
600
|
+
- **totalFollowers** (number)
|
|
601
|
+
- **start** (number)
|
|
602
|
+
- **hasMore** (boolean)
|
|
603
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
604
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
605
|
+
|
|
348
606
|
### getLimits
|
|
349
607
|
|
|
350
608
|
Get current LinkedIn rate limit status for all action types. 0 credits.
|
|
@@ -353,6 +611,13 @@ Get current LinkedIn rate limit status for all action types. 0 credits.
|
|
|
353
611
|
|
|
354
612
|
No parameters.
|
|
355
613
|
|
|
614
|
+
**Returns:**
|
|
615
|
+
|
|
616
|
+
- **multiplier** (number) — Workspace limit multiplier applied to all base limits (default 1.0)
|
|
617
|
+
- **limits** (object)
|
|
618
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
619
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
620
|
+
|
|
356
621
|
### getCredits
|
|
357
622
|
|
|
358
623
|
Get current BeReach credit balance including used, remaining, and percentage. 0 credits.
|
|
@@ -361,6 +626,16 @@ Get current BeReach credit balance including used, remaining, and percentage. 0
|
|
|
361
626
|
|
|
362
627
|
No parameters.
|
|
363
628
|
|
|
629
|
+
**Returns:**
|
|
630
|
+
|
|
631
|
+
- **credits** (object)
|
|
632
|
+
- **current** (number) — Number of credits used in the current billing period
|
|
633
|
+
- **limit** (number) — Maximum credits available for the workspace
|
|
634
|
+
- **remaining** (number) — Credits remaining before hitting the limit
|
|
635
|
+
- **percentage** (number)
|
|
636
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
637
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
638
|
+
|
|
364
639
|
## campaigns
|
|
365
640
|
|
|
366
641
|
### getStatus
|
|
@@ -372,6 +647,12 @@ Query per-profile action status within a campaign. Returns which actions have be
|
|
|
372
647
|
- **campaignSlug** (string, required) — Campaign identifier.
|
|
373
648
|
- **profiles** (string[], required) — LinkedIn profile URLs or URNs to check status for.
|
|
374
649
|
|
|
650
|
+
**Returns:**
|
|
651
|
+
|
|
652
|
+
- **profiles** (array)
|
|
653
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
654
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
655
|
+
|
|
375
656
|
### syncActions
|
|
376
657
|
|
|
377
658
|
Mark actions as completed without performing them on LinkedIn. Use when actions were performed outside the API. 0 credits.
|
|
@@ -381,6 +662,12 @@ Mark actions as completed without performing them on LinkedIn. Use when actions
|
|
|
381
662
|
- **campaignSlug** (string, required) — Campaign identifier.
|
|
382
663
|
- **profiles** (object[], required) — Profiles and actions to mark as completed. Values: message, reply, like, visit, connect.
|
|
383
664
|
|
|
665
|
+
**Returns:**
|
|
666
|
+
|
|
667
|
+
- **synced** (array)
|
|
668
|
+
- **creditsUsed** (number) — Credits consumed by this call.
|
|
669
|
+
- **retryAfter** (number) — Seconds to wait before next call.
|
|
670
|
+
|
|
384
671
|
### getStats
|
|
385
672
|
|
|
386
673
|
Get aggregate campaign statistics: per-action counts, unique profiles, and total credits used. 0 credits.
|
|
@@ -388,3 +675,10 @@ Get aggregate campaign statistics: per-action counts, unique profiles, and total
|
|
|
388
675
|
`client.campaigns.getStats(params)`
|
|
389
676
|
|
|
390
677
|
- **campaignSlug** (string, required) — Campaign identifier.
|
|
678
|
+
|
|
679
|
+
**Returns:**
|
|
680
|
+
|
|
681
|
+
- **stats** (Record<string, …>)
|
|
682
|
+
- **totalProfiles** (number) — Unique profiles processed in this campaign
|
|
683
|
+
- **creditsUsed** (number) — Total credits consumed by this campaign (sum of action counts). Also serves as the per-call creditsUsed (always 0 for this endpoint).
|
|
684
|
+
- **retryAfter** (number) — Seconds to wait before next call of the same type (always 0 for campaign queries).
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bereach-lead-magnet
|
|
3
3
|
description: "Lead magnet workflow — deliver a resource to everyone who engages with a LinkedIn post."
|
|
4
|
-
lastUpdatedAt:
|
|
4
|
+
lastUpdatedAt: 1772673508
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# BeReach Lead Magnet Skill
|
|
8
8
|
|
|
9
|
-
> Sub-skill of [BeReach](
|
|
9
|
+
> Sub-skill of [BeReach](SKILL.md). Deliver a resource to everyone who engages with a LinkedIn post.
|
|
10
10
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
13
|
-
Requires [BeReach SKILL.md](
|
|
13
|
+
Requires [BeReach SKILL.md](SKILL.md) (constraints, SDK setup, anti-hallucination rules) and [SDK Reference](sdk-reference.md) for method signatures and parameters. Do NOT generate code without both loaded.
|
|
14
14
|
|
|
15
15
|
## Script generation rules
|
|
16
16
|
|
|
@@ -22,28 +22,64 @@ All script output and chatbot messages are **for the end user**. Write like you'
|
|
|
22
22
|
|
|
23
23
|
## Objective
|
|
24
24
|
|
|
25
|
-
**Every person who engages with the post must receive the resource via DM as soon as they are distance 1.** Comment,
|
|
25
|
+
**Every person who engages with the post must receive the resource via DM as soon as they are distance 1.** Comment, invitation — the channel doesn't matter. The person gets the resource the moment a DM is possible.
|
|
26
26
|
|
|
27
27
|
## Configuration
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
All campaigns are defined in a single JSON file: `~/.bereach/lead-magnet.json`. The agent creates or edits this file when adding campaigns. On first run: if the file is missing or empty, the agent asks the user for campaign data and writes the file.
|
|
30
30
|
|
|
31
|
+
### Schema
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"campaigns": [
|
|
36
|
+
{
|
|
37
|
+
"campaignSlug": "lm-feb-2026",
|
|
38
|
+
"postUrl": "https://linkedin.com/feed/update/urn:li:activity:...",
|
|
39
|
+
"resourceLink": "https://example.com/resource",
|
|
40
|
+
"dmTemplate": "{firstName}, voici la ressource: {link}",
|
|
41
|
+
"dmExamples": ["Hey {firstName}, c'est ici: {link}", "..."],
|
|
42
|
+
"commentExamples": {
|
|
43
|
+
"distance1": ["C'est parti !", "Envoi en cours"],
|
|
44
|
+
"distance2": ["Connecte-toi pour l'avoir", "On se connecte ?"]
|
|
45
|
+
},
|
|
46
|
+
"language": "fr",
|
|
47
|
+
"timezone": "Europe/Paris",
|
|
48
|
+
"activeHours": { "start": "07:00", "end": "23:00" },
|
|
49
|
+
"enabled": true
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
31
53
|
```
|
|
32
|
-
POST_URL=<LinkedIn post URL>
|
|
33
|
-
CAMPAIGN_SLUG=<unique name, e.g. "lm-feb-2026">
|
|
34
|
-
DM_TEMPLATE=<DM with {firstName} and {link} placeholders, e.g. "{firstName}, voici la ressource: {link}">
|
|
35
|
-
RESOURCE_LINK=<URL to share, or empty if DM is plain text>
|
|
36
|
-
```
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
### Per-campaign fields
|
|
56
|
+
|
|
57
|
+
| Field | Required | Description |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| campaignSlug | yes | Unique ID for dedup |
|
|
60
|
+
| postUrl | yes | LinkedIn post URL |
|
|
61
|
+
| resourceLink | yes/no | URL to share in DM; empty = plain text, disables DM guard |
|
|
62
|
+
| dmTemplate | yes | Base pattern with `{firstName}`, `{link}` |
|
|
63
|
+
| dmExamples | no | List of DM variations (random pick); if absent, generate from template |
|
|
64
|
+
| commentExamples | no | `{ distance1: string[], distance2: string[] }`; if absent, use defaults |
|
|
65
|
+
| language | no | Post language (default: detect from post) |
|
|
66
|
+
| timezone | no | User timezone (default: from getLinkedInProfile) |
|
|
67
|
+
| activeHours | no | `{ start, end }` (default: 07:00–23:00) |
|
|
68
|
+
| enabled | no | `true` = run (default: true) |
|
|
69
|
+
|
|
70
|
+
Ask the user if their DM includes a link to share. If yes, set `resourceLink` — this enables the DM guard (inbox dedup). If no link, set `resourceLink` to empty — the DM guard is disabled and only server-side `campaignSlug` dedup applies.
|
|
71
|
+
|
|
72
|
+
**Validation**: if `resourceLink` is empty or missing, `dmTemplate` must NOT contain `{link}`. If it does, strip `{link}` from the template at runtime and warn the user.
|
|
39
73
|
|
|
40
|
-
|
|
74
|
+
## Config validation
|
|
75
|
+
|
|
76
|
+
At script start, validate the config: reject invalid JSON, require all required fields per campaign, reject duplicate `campaignSlug` values. Skip campaigns with missing or malformed data — never silently ignore errors.
|
|
41
77
|
|
|
42
78
|
## Tracking
|
|
43
79
|
|
|
44
80
|
BeReach is the source of truth for action tracking (`campaignSlug` dedup).
|
|
45
81
|
|
|
46
|
-
For the incremental comment check (`previousTotal`), use
|
|
82
|
+
For the incremental comment check (`previousTotal`), use `~/.bereach/lead-magnet-state.json` with `{ [campaignSlug]: { previousTotal } }` — this survives process restarts and avoids re-processing if the cache resets. Only store the comment total counter locally; all action tracking stays on BeReach.
|
|
47
83
|
|
|
48
84
|
- **Check status**: `getStatus` with `campaignSlug` and `profiles` → returns per-profile booleans (`message`, `reply`, `like`, `visit`, `connect`)
|
|
49
85
|
- **Manual sync**: `syncActions` with `campaignSlug` and `profiles: [{ profile, actions }]` → mark actions as completed without performing them (e.g. manual DMs done outside the bot)
|
|
@@ -58,23 +94,36 @@ All 3 scripts must perform pre-flight at the start of each run:
|
|
|
58
94
|
|
|
59
95
|
## Architecture
|
|
60
96
|
|
|
61
|
-
|
|
97
|
+
**3 scripts** (one per workflow): `lm-comments.ts`, `lm-invitations.ts`, `lm-connections.ts`. **3 scheduled runs** (global) — each runs one script that loops over all enabled campaigns.
|
|
98
|
+
|
|
99
|
+
Each script:
|
|
62
100
|
|
|
63
|
-
1.
|
|
64
|
-
2.
|
|
65
|
-
3.
|
|
101
|
+
1. Reads `~/.bereach/lead-magnet.json`
|
|
102
|
+
2. Filters `campaigns` where `enabled !== false`
|
|
103
|
+
3. For each campaign: acquire lock `lm-{slug}-{script}` (see "Lock mechanism" below). If locked, skip that campaign — a previous run is still active.
|
|
104
|
+
4. Runs its logic for the campaign.
|
|
105
|
+
5. Prints recap per campaign, releases lock, moves to next campaign.
|
|
106
|
+
6. At end: aggregate recap for all campaigns.
|
|
66
107
|
|
|
67
|
-
|
|
108
|
+
BeReach dedup ensures no action is performed twice across scripts (same `campaignSlug`).
|
|
68
109
|
|
|
69
110
|
### Multiple campaigns
|
|
70
111
|
|
|
71
|
-
|
|
112
|
+
All campaigns run in the same 3 scheduled runs. The config file lists all campaigns; each script iterates over them. Lock IDs: `lm-{slug}-comments`, `lm-{slug}-invitations`, `lm-{slug}-connections` (per campaign). State: `~/.bereach/lead-magnet-state.json` with `{ [campaignSlug]: { previousTotal } }`.
|
|
72
113
|
|
|
73
114
|
All campaigns share the same LinkedIn rate limits. If multiple campaigns run at the same time, they compete for the same quota. BeReach handles conflicts automatically.
|
|
74
115
|
|
|
116
|
+
### Lock mechanism
|
|
117
|
+
|
|
118
|
+
Before running a script for a campaign, acquire a lock to prevent concurrent runs. One lock per campaign per script.
|
|
119
|
+
|
|
120
|
+
- If the lock is already held by an active process, skip that campaign.
|
|
121
|
+
- If the lock is stale (older than 30 minutes) or the holding process is dead, force-acquire.
|
|
122
|
+
- Always release the lock on exit (normal, error, or signal).
|
|
123
|
+
|
|
75
124
|
### Script 1 — Comments
|
|
76
125
|
|
|
77
|
-
|
|
126
|
+
For each campaign (from config): scrape commenters and engage them.
|
|
78
127
|
|
|
79
128
|
1. **Check first** (mandatory): `collectComments` with `count: 0` → store `total`. On subsequent rounds, compare with previous `total`. If unchanged, skip this round. This saves credits and rate limit slots.
|
|
80
129
|
2. **Fetch**: `collectComments` with `campaignSlug` → response: `profiles[]` (NOT "comments"). Paginate: while `hasMore`, increment `start` by `count`.
|
|
@@ -92,17 +141,18 @@ Scrape commenters and engage them.
|
|
|
92
141
|
|
|
93
142
|
### Script 2 — Invitations
|
|
94
143
|
|
|
95
|
-
Accept pending invitations and deliver the resource.
|
|
144
|
+
Accept pending invitations and deliver the resource to campaign-related invitees.
|
|
96
145
|
|
|
97
146
|
- `listInvitations` → response: `invitations[]`. Each has: `invitationId`, `sharedSecret`, `fromMember: { name, profileUrl }`. Paginate: while `total > start + count`.
|
|
147
|
+
- **Campaign attribution**: `listInvitations` returns ALL pending invitations. Cross-reference each inviter against campaign commenter lists to find which campaign (if any) they belong to. Accept all invitations, but only DM if campaign-matched.
|
|
98
148
|
- For each invitation:
|
|
99
|
-
1. `acceptInvitation` with `invitationId` and `sharedSecret`
|
|
100
|
-
2. `visitProfile` with `
|
|
101
|
-
3. If
|
|
149
|
+
1. `acceptInvitation` with `invitationId` and `sharedSecret`
|
|
150
|
+
2. If campaign matched: `visitProfile` with matched `campaignSlug`, then **DM dedup** → DM if not already sent.
|
|
151
|
+
3. If no campaign match (organic invitation): accept only, no DM.
|
|
102
152
|
|
|
103
153
|
### Script 3 — New connections
|
|
104
154
|
|
|
105
|
-
|
|
155
|
+
For each campaign (from config): check if distance 2+ commenters have now connected.
|
|
106
156
|
|
|
107
157
|
- Re-fetch commenters: `collectComments` with `campaignSlug` — paginate all
|
|
108
158
|
- Filter: `actionsCompleted.message === false` and `knownDistance > 1` (skip profiles where `knownDistance` is null — they haven't been visited yet and belong to Script 1)
|
|
@@ -122,9 +172,9 @@ Check in order. If either says "already sent", skip the DM.
|
|
|
122
172
|
|
|
123
173
|
**Layer 1 — `actionsCompleted.message`** (free, within campaign). Already checked in the script loop — `true` → skip.
|
|
124
174
|
|
|
125
|
-
**Layer 2 — `/find` inbox lookup** (0 credits, cross-campaign). Only when `
|
|
175
|
+
**Layer 2 — `/find` inbox lookup** (0 credits, cross-campaign). Only when `resourceLink` is set. Call `findConversation` with `includeMessages: true` → `{ found, messages }`. `messages` is at the **top level**, not nested. If any message text contains `resourceLink`: skip DM, call `syncActions` to mark done. If found but no link in messages: pass `messages` as context for DM tone adaptation. The function must return both skip decision and messages.
|
|
126
176
|
|
|
127
|
-
**Fail-safe**: if `findConversation` errors or all retries fail, **skip this profile** — do NOT send the DM. A failed lookup treated as "not found" causes duplicates. The profile will be retried next
|
|
177
|
+
**Fail-safe**: if `findConversation` errors or all retries fail, **skip this profile** — do NOT send the DM. A failed lookup treated as "not found" causes duplicates. The profile will be retried next run.
|
|
128
178
|
|
|
129
179
|
## Language
|
|
130
180
|
|
|
@@ -134,21 +184,21 @@ For lead magnet, the default language is the **post language** — everyone comm
|
|
|
134
184
|
|
|
135
185
|
- Distance 1: acknowledge, tell them you're sending it / it's in their DMs
|
|
136
186
|
- Distance 2+: tell them to connect with you to receive it. Never mention DMs.
|
|
137
|
-
-
|
|
187
|
+
- Use `commentExamples.distance1` and `commentExamples.distance2` from config if present; else generate from language (at least 5 per distance per language).
|
|
138
188
|
- Short, casual, 3-5 words.
|
|
139
|
-
-
|
|
189
|
+
- Random selection over the pool. Vary phrasing and tone to avoid LinkedIn spam detection.
|
|
140
190
|
|
|
141
191
|
## DM content
|
|
142
192
|
|
|
143
|
-
- Use `
|
|
144
|
-
- Language = post language.
|
|
145
|
-
- **When conversation history exists** (DM guard fetched messages, no `
|
|
193
|
+
- Use `dmExamples` from config if present (random pick); else generate at least 10 variations from `dmTemplate` at boot (different wording, greeting, emoji, sentence structure). Pick a random variation for each DM. Never send the same literal text twice in a row.
|
|
194
|
+
- Language = post language (from config or detect from post).
|
|
195
|
+
- **When conversation history exists** (DM guard fetched messages, no `resourceLink` found): adapt the tone — more casual, shorter. The conversation messages are context.
|
|
146
196
|
|
|
147
197
|
## Time window
|
|
148
198
|
|
|
149
|
-
Scripts only run between **7:00 and 23:00** in the user's timezone (default). At the start of each run, check the current local time. If outside the window,
|
|
199
|
+
Scripts only run between **7:00 and 23:00** in the user's timezone (default). Use `activeHours` from config if present; else default 07:00–23:00. At the start of each run (per campaign), check the current local time. If outside the window, skip that campaign with a short log ("Outside active hours, skipping").
|
|
150
200
|
|
|
151
|
-
|
|
201
|
+
Use `timezone` from config if present; else detect from `getLinkedInProfile` → `location` (e.g. "Paris, France" → Europe/Paris). If ambiguous, ask the user once and store in the campaign config.
|
|
152
202
|
|
|
153
203
|
## Pacing
|
|
154
204
|
|
|
@@ -156,21 +206,21 @@ Follow pacing rules from the main skill (Constraints #2). No exceptions — slee
|
|
|
156
206
|
|
|
157
207
|
## Cron
|
|
158
208
|
|
|
159
|
-
Three
|
|
209
|
+
Three global crons. Config at `~/.openclaw/lead-magnet.json`. Each cron runs one script that loops over all enabled campaigns.
|
|
160
210
|
|
|
161
211
|
All crons MUST use `"sessionTarget": "spawn"` to avoid blocking the main session. Each spawned sub-agent runs the script independently and pushes the recap back to the user when done.
|
|
162
212
|
|
|
163
213
|
```json
|
|
164
|
-
{ "id": "lm-
|
|
165
|
-
{ "id": "lm-
|
|
166
|
-
{ "id": "lm-
|
|
214
|
+
{ "id": "lm-comments", "every": "1h", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run lm-comments.ts and report recap" }
|
|
215
|
+
{ "id": "lm-invitations", "every": "1h", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run lm-invitations.ts and report recap" }
|
|
216
|
+
{ "id": "lm-connections", "every": "1h", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run lm-connections.ts and report recap" }
|
|
167
217
|
```
|
|
168
218
|
|
|
169
|
-
Each script acquires a
|
|
219
|
+
Each script acquires a per-campaign lock (`lm-{slug}-comments`, etc.) before running that campaign. If the lock is held (previous run still active), skip that campaign — do NOT queue or wait.
|
|
170
220
|
|
|
171
221
|
### Pause / Resume
|
|
172
222
|
|
|
173
|
-
The user can pause or resume any
|
|
223
|
+
The user can pause or resume any campaign by setting `enabled: false` in `~/.bereach/lead-magnet.json` for that campaign.
|
|
174
224
|
|
|
175
225
|
### Recap (print after each run)
|
|
176
226
|
|
|
@@ -196,5 +246,5 @@ All enabled by default. The user can ask to disable any of these to save credits
|
|
|
196
246
|
- **Likes** on comments (saves 1 credit/comment)
|
|
197
247
|
- **Connection re-checks** (saves visit + DM credits)
|
|
198
248
|
- **Comment replies** (saves 1 credit/comment)
|
|
199
|
-
- **DM guard** — conversation lookup before DMing (0 credits, auto-disabled when no `
|
|
249
|
+
- **DM guard** — conversation lookup before DMing (0 credits, auto-disabled when no `resourceLink`)
|
|
200
250
|
- **Reply guard** — hasReplyFromPostAuthor check (free, but can be skipped if user wants to re-reply)
|
package/src/tools/definitions.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool definitions generated from the BeReach OpenAPI spec.
|
|
3
3
|
*
|
|
4
|
-
* Source of truth: apps/
|
|
5
|
-
* apps/
|
|
4
|
+
* Source of truth: apps/web/src/lib/openapi/schemas/linkedin.ts
|
|
5
|
+
* apps/web/src/lib/openapi/schemas/campaigns.ts
|
|
6
6
|
*
|
|
7
7
|
* Each definition maps 1:1 to an OpenAPI endpoint and an SDK method.
|
|
8
8
|
* Deprecated fields (actionSlug) are excluded.
|
package/src/tools/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import type { Bereach } from "bereach";
|
|
1
2
|
import { getClient } from "./../client";
|
|
2
3
|
import { definitions } from "./definitions";
|
|
3
4
|
|
|
5
|
+
type ResourceMap = {
|
|
6
|
+
[K in keyof Bereach as Bereach[K] extends object ? K : never]: Bereach[K];
|
|
7
|
+
};
|
|
8
|
+
|
|
4
9
|
/**
|
|
5
10
|
* Registers all 33 BeReach tools with the OpenClaw agent.
|
|
6
11
|
* Format per https://docs.openclaw.ai/plugins/agent-tools
|
|
@@ -15,7 +20,9 @@ export function registerAllTools(api: any) {
|
|
|
15
20
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
16
21
|
const client = getClient();
|
|
17
22
|
const [resource, method] = def.handler.split(".");
|
|
18
|
-
const
|
|
23
|
+
const res = client[resource as keyof ResourceMap];
|
|
24
|
+
const fn = (res as Record<string, (...args: unknown[]) => Promise<unknown>>)[method];
|
|
25
|
+
const result = await fn(params);
|
|
19
26
|
return {
|
|
20
27
|
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
21
28
|
};
|
package/scripts/test-register.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
/// <reference types="node" />
|
|
3
|
-
/**
|
|
4
|
-
* Simulates OpenClaw plugin registration to catch config/trim errors.
|
|
5
|
-
* Run: pnpm exec tsx scripts/test-register.ts
|
|
6
|
-
* Or: BEREACH_API_KEY=brc_your_key pnpm exec tsx scripts/test-register.ts
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import register from "../src/index";
|
|
10
|
-
|
|
11
|
-
const apiKey = process.env.BEREACH_API_KEY || "brc_test_placeholder";
|
|
12
|
-
|
|
13
|
-
const mockApi = {
|
|
14
|
-
pluginConfig: { BEREACH_API_KEY: apiKey },
|
|
15
|
-
registerTool: () => {},
|
|
16
|
-
registerCommand: () => {},
|
|
17
|
-
registerCli: () => {},
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
console.log("Testing plugin register with config (simulates OpenClaw load)...");
|
|
21
|
-
try {
|
|
22
|
-
register(mockApi);
|
|
23
|
-
console.log("✓ register() completed — no trim/undefined errors");
|
|
24
|
-
} catch (err: any) {
|
|
25
|
-
console.error("✗ register() failed:", err?.message || err);
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
console.log("\nTesting plugin register WITHOUT config (should load, fail on first use)...");
|
|
30
|
-
try {
|
|
31
|
-
register({ pluginConfig: undefined, registerTool: () => {}, registerCommand: () => {}, registerCli: () => {} });
|
|
32
|
-
console.log("✓ register() completed — plugin loads without config (lazy init)");
|
|
33
|
-
} catch (err: any) {
|
|
34
|
-
console.error("✗ register() failed with missing config:", err?.message || err);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
console.log("\n✓ All tests passed");
|