bereach-openclaw 0.3.4 → 1.2.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 +14 -14
- package/__tests__/helpers.test.ts +72 -27
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
- package/skills/bereach/SKILL.md +52 -17
- package/skills/bereach/openclaw-optimization.md +1 -12
- package/skills/bereach/sdk-reference.md +1145 -67
- package/skills/bereach/sub/lead-gen.md +372 -0
- package/skills/bereach/sub/lead-magnet.md +61 -20
- package/skills/bereach/sub/outreach.md +275 -0
- package/src/index.ts +1 -1
- package/src/lead-magnet/helpers.ts +13 -10
- package/src/tools/definitions.ts +499 -15
- package/src/tools/index.ts +67 -8
package/README.md
CHANGED
|
@@ -16,13 +16,13 @@ openclaw plugins install bereach-openclaw
|
|
|
16
16
|
|
|
17
17
|
## Upgrade
|
|
18
18
|
|
|
19
|
-
`openclaw plugins update bereach` can leave `node_modules` and `extensions/` out of sync (version mismatch → trim/crash errors). **Use uninstall + reinstall** instead.
|
|
19
|
+
`openclaw plugins update bereach-openclaw` can leave `node_modules` and `extensions/` out of sync (version mismatch → trim/crash errors). **Use uninstall + reinstall** instead.
|
|
20
20
|
|
|
21
|
-
**Before upgrading:** note your `BEREACH_API_KEY` — uninstall may remove `plugins.entries.bereach`.
|
|
21
|
+
**Before upgrading:** note your `BEREACH_API_KEY` — uninstall may remove `plugins.entries.bereach-openclaw`.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
24
|
# 1. Uninstall
|
|
25
|
-
openclaw plugins uninstall bereach
|
|
25
|
+
openclaw plugins uninstall bereach-openclaw
|
|
26
26
|
|
|
27
27
|
# 2. Reinstall latest
|
|
28
28
|
openclaw plugins install bereach-openclaw
|
|
@@ -35,7 +35,7 @@ openclaw plugins install bereach-openclaw
|
|
|
35
35
|
Verify versions match:
|
|
36
36
|
```bash
|
|
37
37
|
cat /data/.openclaw/node_modules/bereach-openclaw/package.json | grep version
|
|
38
|
-
cat /data/.openclaw/extensions/bereach/package.json | grep version
|
|
38
|
+
cat /data/.openclaw/extensions/bereach-openclaw/package.json | grep version
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Setup
|
|
@@ -47,9 +47,9 @@ The API key can be set in 3 ways (in order of precedence):
|
|
|
47
47
|
```json
|
|
48
48
|
{
|
|
49
49
|
"plugins": {
|
|
50
|
-
"allow": ["bereach"],
|
|
50
|
+
"allow": ["bereach-openclaw"],
|
|
51
51
|
"entries": {
|
|
52
|
-
"bereach": {
|
|
52
|
+
"bereach-openclaw": {
|
|
53
53
|
"enabled": true,
|
|
54
54
|
"config": {
|
|
55
55
|
"BEREACH_API_KEY": "brc_your_token_here"
|
|
@@ -104,13 +104,13 @@ If that doesn't work:
|
|
|
104
104
|
|
|
105
105
|
OpenClaw loads plugins from **two locations**:
|
|
106
106
|
- `node_modules/bereach-openclaw/` — the npm package (source)
|
|
107
|
-
- `extensions/bereach/` — the active copy OpenClaw uses at runtime
|
|
107
|
+
- `extensions/bereach-openclaw/` — the active copy OpenClaw uses at runtime
|
|
108
108
|
|
|
109
|
-
If `extensions/bereach/` is corrupt or incomplete, you get trim/undefined errors. Fix:
|
|
109
|
+
If `extensions/bereach-openclaw/` is corrupt or incomplete, you get trim/undefined errors. Fix:
|
|
110
110
|
|
|
111
111
|
**1. Backup and remove the active extension:**
|
|
112
112
|
```bash
|
|
113
|
-
mv /data/.openclaw/extensions/bereach /data/.openclaw/extensions/bereach.bak.$(date +%s)
|
|
113
|
+
mv /data/.openclaw/extensions/bereach-openclaw /data/.openclaw/extensions/bereach-openclaw.bak.$(date +%s)
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
**2. Reinstall from npm (inside the container):**
|
|
@@ -119,18 +119,18 @@ cd /data/.openclaw
|
|
|
119
119
|
npm i bereach-openclaw@latest
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
**3. Restart OpenClaw** so it rebuilds `extensions/bereach/` from the package.
|
|
122
|
+
**3. Restart OpenClaw** so it rebuilds `extensions/bereach-openclaw/` from the package.
|
|
123
123
|
|
|
124
|
-
**4. If `extensions/bereach/` is still incomplete after restart**, force a symlink:
|
|
124
|
+
**4. If `extensions/bereach-openclaw/` is still incomplete after restart**, force a symlink:
|
|
125
125
|
```bash
|
|
126
|
-
rm -rf /data/.openclaw/extensions/bereach
|
|
127
|
-
ln -s /data/.openclaw/node_modules/bereach-openclaw /data/.openclaw/extensions/bereach
|
|
126
|
+
rm -rf /data/.openclaw/extensions/bereach-openclaw
|
|
127
|
+
ln -s /data/.openclaw/node_modules/bereach-openclaw /data/.openclaw/extensions/bereach-openclaw
|
|
128
128
|
# then restart the container
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
**5. Verify** — compare the two manifests:
|
|
132
132
|
```bash
|
|
133
|
-
wc -l /data/.openclaw/extensions/bereach/openclaw.plugin.json
|
|
133
|
+
wc -l /data/.openclaw/extensions/bereach-openclaw/openclaw.plugin.json
|
|
134
134
|
wc -l /data/.openclaw/node_modules/bereach-openclaw/openclaw.plugin.json
|
|
135
135
|
```
|
|
136
136
|
They should match. Per [OpenClaw Plugin Manifest](https://docs.openclaw.ai/plugins/manifest), tools are registered at runtime via `api.registerTool()`.
|
|
@@ -28,19 +28,22 @@ function createMockClient(overrides: Partial<Record<string, Record<string, unkno
|
|
|
28
28
|
return {
|
|
29
29
|
linkedinScrapers: {
|
|
30
30
|
collectComments: vi.fn(),
|
|
31
|
-
visitProfile: vi.fn(),
|
|
32
31
|
...overrides.linkedinScrapers,
|
|
33
32
|
},
|
|
33
|
+
scrapers: {
|
|
34
|
+
visitProfile: vi.fn(),
|
|
35
|
+
...overrides.scrapers,
|
|
36
|
+
},
|
|
34
37
|
linkedinActions: {
|
|
35
38
|
listInvitations: vi.fn(),
|
|
36
39
|
...overrides.linkedinActions,
|
|
37
40
|
},
|
|
38
|
-
|
|
41
|
+
chat: {
|
|
39
42
|
findConversation: vi.fn(),
|
|
40
|
-
...overrides.
|
|
43
|
+
...overrides.chat,
|
|
41
44
|
},
|
|
42
45
|
campaigns: {
|
|
43
|
-
|
|
46
|
+
sync: vi.fn(),
|
|
44
47
|
getStats: vi.fn(),
|
|
45
48
|
...overrides.campaigns,
|
|
46
49
|
},
|
|
@@ -230,20 +233,62 @@ describe("collectAllComments", () => {
|
|
|
230
233
|
expect(mock).toHaveBeenCalledWith({ postUrl: "https://post", count: 0 });
|
|
231
234
|
});
|
|
232
235
|
|
|
233
|
-
it("fetches
|
|
236
|
+
it("fetches from previousTotal offset when total differs", async () => {
|
|
234
237
|
const client = createMockClient();
|
|
235
238
|
const mock = vi.mocked(client.linkedinScrapers.collectComments);
|
|
236
239
|
|
|
237
240
|
mock
|
|
238
241
|
.mockResolvedValueOnce(commentsPage([], { total: 55, start: 0, hasMore: false }))
|
|
239
242
|
.mockResolvedValueOnce(
|
|
240
|
-
commentsPage([{ name: "
|
|
243
|
+
commentsPage([{ name: "new1" }, { name: "new2" }], { total: 55, start: 50, hasMore: false }),
|
|
241
244
|
);
|
|
242
245
|
|
|
243
246
|
const result = await collectAllComments(client, "https://post", "slug", { previousTotal: 50 });
|
|
244
247
|
|
|
245
248
|
expect(result.skipped).toBe(false);
|
|
249
|
+
expect(result.profiles).toHaveLength(2);
|
|
246
250
|
expect(mock).toHaveBeenCalledTimes(2);
|
|
251
|
+
expect(mock).toHaveBeenNthCalledWith(1, { postUrl: "https://post", count: 0 });
|
|
252
|
+
expect(mock).toHaveBeenNthCalledWith(2, { postUrl: "https://post", start: 50, count: 100, campaignSlug: "slug" });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("fetches only new comments (previousTotal=500, total=513)", async () => {
|
|
256
|
+
const client = createMockClient();
|
|
257
|
+
const mock = vi.mocked(client.linkedinScrapers.collectComments);
|
|
258
|
+
|
|
259
|
+
const newProfiles = Array.from({ length: 13 }, (_, i) => ({ name: `new${i}` }));
|
|
260
|
+
|
|
261
|
+
mock
|
|
262
|
+
.mockResolvedValueOnce(commentsPage([], { total: 513, start: 0, hasMore: false }))
|
|
263
|
+
.mockResolvedValueOnce(commentsPage(newProfiles, { total: 513, start: 500, hasMore: false }));
|
|
264
|
+
|
|
265
|
+
const result = await collectAllComments(client, "https://post", "slug", { previousTotal: 500 });
|
|
266
|
+
|
|
267
|
+
expect(result.profiles).toHaveLength(13);
|
|
268
|
+
expect(result.total).toBe(513);
|
|
269
|
+
expect(mock).toHaveBeenCalledTimes(2);
|
|
270
|
+
expect(mock).toHaveBeenNthCalledWith(2, { postUrl: "https://post", start: 500, count: 100, campaignSlug: "slug" });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("paginates from previousTotal across multiple pages (200→350)", async () => {
|
|
274
|
+
const client = createMockClient();
|
|
275
|
+
const mock = vi.mocked(client.linkedinScrapers.collectComments);
|
|
276
|
+
|
|
277
|
+
const page1 = Array.from({ length: 100 }, (_, i) => ({ name: `p${200 + i}` }));
|
|
278
|
+
const page2 = Array.from({ length: 50 }, (_, i) => ({ name: `p${300 + i}` }));
|
|
279
|
+
|
|
280
|
+
mock
|
|
281
|
+
.mockResolvedValueOnce(commentsPage([], { total: 350, start: 0, hasMore: false }))
|
|
282
|
+
.mockResolvedValueOnce(commentsPage(page1, { total: 350, start: 200, hasMore: true }))
|
|
283
|
+
.mockResolvedValueOnce(commentsPage(page2, { total: 350, start: 300, hasMore: false }));
|
|
284
|
+
|
|
285
|
+
const result = await collectAllComments(client, "https://post", "slug", { previousTotal: 200 });
|
|
286
|
+
|
|
287
|
+
expect(result.profiles).toHaveLength(150);
|
|
288
|
+
expect(result.total).toBe(350);
|
|
289
|
+
expect(mock).toHaveBeenCalledTimes(3);
|
|
290
|
+
expect(mock).toHaveBeenNthCalledWith(2, { postUrl: "https://post", start: 200, count: 100, campaignSlug: "slug" });
|
|
291
|
+
expect(mock).toHaveBeenNthCalledWith(3, { postUrl: "https://post", start: 300, count: 100, campaignSlug: "slug" });
|
|
247
292
|
});
|
|
248
293
|
|
|
249
294
|
it("handles empty post (0 comments)", async () => {
|
|
@@ -344,7 +389,7 @@ describe("listAllInvitations", () => {
|
|
|
344
389
|
describe("visitProfileIfNeeded", () => {
|
|
345
390
|
it("returns knownDistance without visiting when set", async () => {
|
|
346
391
|
const client = createMockClient();
|
|
347
|
-
const mock = vi.mocked(client.
|
|
392
|
+
const mock = vi.mocked(client.scrapers.visitProfile);
|
|
348
393
|
|
|
349
394
|
const result = await visitProfileIfNeeded(client, "u/alice", "slug", 1);
|
|
350
395
|
|
|
@@ -356,7 +401,7 @@ describe("visitProfileIfNeeded", () => {
|
|
|
356
401
|
|
|
357
402
|
it("returns knownDistance=0 without visiting (0 is valid)", async () => {
|
|
358
403
|
const client = createMockClient();
|
|
359
|
-
const mock = vi.mocked(client.
|
|
404
|
+
const mock = vi.mocked(client.scrapers.visitProfile);
|
|
360
405
|
|
|
361
406
|
const result = await visitProfileIfNeeded(client, "u/alice", "slug", 0);
|
|
362
407
|
|
|
@@ -367,7 +412,7 @@ describe("visitProfileIfNeeded", () => {
|
|
|
367
412
|
|
|
368
413
|
it("returns knownDistance=2 without visiting", async () => {
|
|
369
414
|
const client = createMockClient();
|
|
370
|
-
const mock = vi.mocked(client.
|
|
415
|
+
const mock = vi.mocked(client.scrapers.visitProfile);
|
|
371
416
|
|
|
372
417
|
const result = await visitProfileIfNeeded(client, "u/alice", "slug", 2);
|
|
373
418
|
|
|
@@ -376,9 +421,9 @@ describe("visitProfileIfNeeded", () => {
|
|
|
376
421
|
expect(mock).not.toHaveBeenCalled();
|
|
377
422
|
});
|
|
378
423
|
|
|
379
|
-
it("calls
|
|
424
|
+
it("calls scrapers.visitProfile when knownDistance is null", async () => {
|
|
380
425
|
const client = createMockClient();
|
|
381
|
-
const mock = vi.mocked(client.
|
|
426
|
+
const mock = vi.mocked(client.scrapers.visitProfile);
|
|
382
427
|
mock.mockResolvedValueOnce({
|
|
383
428
|
success: true,
|
|
384
429
|
firstName: "Alice",
|
|
@@ -407,7 +452,7 @@ describe("visitProfileIfNeeded", () => {
|
|
|
407
452
|
|
|
408
453
|
it("calls visitProfile when knownDistance is undefined", async () => {
|
|
409
454
|
const client = createMockClient();
|
|
410
|
-
const mock = vi.mocked(client.
|
|
455
|
+
const mock = vi.mocked(client.scrapers.visitProfile);
|
|
411
456
|
mock.mockResolvedValueOnce({
|
|
412
457
|
success: true,
|
|
413
458
|
firstName: "Bob",
|
|
@@ -432,7 +477,7 @@ describe("visitProfileIfNeeded", () => {
|
|
|
432
477
|
|
|
433
478
|
it("handles null memberDistance from visit response", async () => {
|
|
434
479
|
const client = createMockClient();
|
|
435
|
-
const mock = vi.mocked(client.
|
|
480
|
+
const mock = vi.mocked(client.scrapers.visitProfile);
|
|
436
481
|
mock.mockResolvedValueOnce({
|
|
437
482
|
success: true,
|
|
438
483
|
firstName: "X",
|
|
@@ -456,7 +501,7 @@ describe("visitProfileIfNeeded", () => {
|
|
|
456
501
|
|
|
457
502
|
it("propagates FatalError from visitProfile", async () => {
|
|
458
503
|
const client = createMockClient();
|
|
459
|
-
const mock = vi.mocked(client.
|
|
504
|
+
const mock = vi.mocked(client.scrapers.visitProfile);
|
|
460
505
|
mock.mockRejectedValueOnce(Object.assign(new Error("unauthorized"), { statusCode: 401 }));
|
|
461
506
|
|
|
462
507
|
await expect(
|
|
@@ -470,7 +515,7 @@ describe("visitProfileIfNeeded", () => {
|
|
|
470
515
|
describe("findConversationForDmGuard", () => {
|
|
471
516
|
it("returns skip=false when no conversation found", async () => {
|
|
472
517
|
const client = createMockClient();
|
|
473
|
-
const mock = vi.mocked(client.
|
|
518
|
+
const mock = vi.mocked(client.chat.findConversation);
|
|
474
519
|
mock.mockResolvedValueOnce({
|
|
475
520
|
success: true,
|
|
476
521
|
found: false,
|
|
@@ -488,8 +533,8 @@ describe("findConversationForDmGuard", () => {
|
|
|
488
533
|
|
|
489
534
|
it("returns skip=true when resourceLink found in messages", async () => {
|
|
490
535
|
const client = createMockClient();
|
|
491
|
-
const findMock = vi.mocked(client.
|
|
492
|
-
const syncMock = vi.mocked(client.campaigns.
|
|
536
|
+
const findMock = vi.mocked(client.chat.findConversation);
|
|
537
|
+
const syncMock = vi.mocked(client.campaigns.sync);
|
|
493
538
|
|
|
494
539
|
findMock.mockResolvedValueOnce({
|
|
495
540
|
success: true,
|
|
@@ -519,7 +564,7 @@ describe("findConversationForDmGuard", () => {
|
|
|
519
564
|
|
|
520
565
|
it("returns skip=false when conversation exists but link not in messages", async () => {
|
|
521
566
|
const client = createMockClient();
|
|
522
|
-
const findMock = vi.mocked(client.
|
|
567
|
+
const findMock = vi.mocked(client.chat.findConversation);
|
|
523
568
|
|
|
524
569
|
findMock.mockResolvedValueOnce({
|
|
525
570
|
success: true,
|
|
@@ -547,7 +592,7 @@ describe("findConversationForDmGuard", () => {
|
|
|
547
592
|
|
|
548
593
|
it("returns messages for tone adaptation when no link found", async () => {
|
|
549
594
|
const client = createMockClient();
|
|
550
|
-
const findMock = vi.mocked(client.
|
|
595
|
+
const findMock = vi.mocked(client.chat.findConversation);
|
|
551
596
|
|
|
552
597
|
findMock.mockResolvedValueOnce({
|
|
553
598
|
success: true,
|
|
@@ -584,7 +629,7 @@ describe("findConversationForDmGuard", () => {
|
|
|
584
629
|
|
|
585
630
|
it("returns skip=true (fail-safe) when findConversation throws", async () => {
|
|
586
631
|
const client = createMockClient();
|
|
587
|
-
const findMock = vi.mocked(client.
|
|
632
|
+
const findMock = vi.mocked(client.chat.findConversation);
|
|
588
633
|
findMock.mockRejectedValueOnce(Object.assign(new Error("network"), { statusCode: 500 }));
|
|
589
634
|
|
|
590
635
|
const result = await findConversationForDmGuard(client, "u/alice", "https://link", "slug");
|
|
@@ -593,10 +638,10 @@ describe("findConversationForDmGuard", () => {
|
|
|
593
638
|
expect(result.messages).toHaveLength(0);
|
|
594
639
|
});
|
|
595
640
|
|
|
596
|
-
it("still returns skip=true when
|
|
641
|
+
it("still returns skip=true when sync fails after finding link", async () => {
|
|
597
642
|
const client = createMockClient();
|
|
598
|
-
const findMock = vi.mocked(client.
|
|
599
|
-
const syncMock = vi.mocked(client.campaigns.
|
|
643
|
+
const findMock = vi.mocked(client.chat.findConversation);
|
|
644
|
+
const syncMock = vi.mocked(client.campaigns.sync);
|
|
600
645
|
|
|
601
646
|
findMock.mockResolvedValueOnce({
|
|
602
647
|
success: true,
|
|
@@ -624,7 +669,7 @@ describe("findConversationForDmGuard", () => {
|
|
|
624
669
|
|
|
625
670
|
it("handles conversation found but messages array is null", async () => {
|
|
626
671
|
const client = createMockClient();
|
|
627
|
-
const findMock = vi.mocked(client.
|
|
672
|
+
const findMock = vi.mocked(client.chat.findConversation);
|
|
628
673
|
|
|
629
674
|
findMock.mockResolvedValueOnce({
|
|
630
675
|
success: true,
|
|
@@ -643,7 +688,7 @@ describe("findConversationForDmGuard", () => {
|
|
|
643
688
|
|
|
644
689
|
it("handles conversation found but messages array is empty", async () => {
|
|
645
690
|
const client = createMockClient();
|
|
646
|
-
const findMock = vi.mocked(client.
|
|
691
|
+
const findMock = vi.mocked(client.chat.findConversation);
|
|
647
692
|
|
|
648
693
|
findMock.mockResolvedValueOnce({
|
|
649
694
|
success: true,
|
|
@@ -662,8 +707,8 @@ describe("findConversationForDmGuard", () => {
|
|
|
662
707
|
|
|
663
708
|
it("matches resourceLink as substring in message text", async () => {
|
|
664
709
|
const client = createMockClient();
|
|
665
|
-
const findMock = vi.mocked(client.
|
|
666
|
-
const syncMock = vi.mocked(client.campaigns.
|
|
710
|
+
const findMock = vi.mocked(client.chat.findConversation);
|
|
711
|
+
const syncMock = vi.mocked(client.campaigns.sync);
|
|
667
712
|
|
|
668
713
|
findMock.mockResolvedValueOnce({
|
|
669
714
|
success: true,
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bereach-openclaw",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "BeReach LinkedIn automation plugin for OpenClaw",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"exports": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"bereach": "^
|
|
18
|
+
"bereach": "^1.2.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^22.19.3",
|
package/skills/bereach/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bereach
|
|
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:
|
|
3
|
+
description: "Automate LinkedIn outreach via BeReach (berea.ch). Use when: prospecting, engaging posts, scraping engagement, searching LinkedIn, managing inbox, running campaigns, managing invitations, analytics, company pages, Sales Navigator, content engagement, feed monitoring. Requires BEREACH_API_KEY."
|
|
4
|
+
lastUpdatedAt: 1773274027
|
|
5
5
|
metadata: { "openclaw": { "requires": { "env": ["BEREACH_API_KEY"] }, "primaryEnv": "BEREACH_API_KEY" } }
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -17,15 +17,19 @@ Automate LinkedIn prospection and engagement via BeReach.
|
|
|
17
17
|
|
|
18
18
|
| Sub-skill | Keywords | URL | lastUpdatedAt |
|
|
19
19
|
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------- |
|
|
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 |
|
|
21
|
-
|
|
|
22
|
-
|
|
|
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 | 1773267546 |
|
|
21
|
+
| Lead Gen | lead gen, find leads, search, qualify, ICP, pipeline, build list, scrape engagement, competitor, grow database, prospecting, hashtag, Sales Navigator | sub/lead-gen.md | 1773269560 |
|
|
22
|
+
| Outreach | outreach, connect, DM, message, follow up, sequence, connection request, reply, personalized outreach, campaign outreach, warming, follow, like post | sub/outreach.md | 1773269560 |
|
|
23
|
+
| OpenClaw Optimization | openclaw, model, opus, sonnet, haiku, config, SOUL.md, heartbeat, prompt caching, AI cost reduction, /model | openclaw-optimization.md | 1773274027 |
|
|
24
|
+
| SDK Reference | sdk, method, parameter, signature, reference, api, script | sdk-reference.md | 1773269560 |
|
|
23
25
|
|
|
24
26
|
### Supporting files
|
|
25
27
|
|
|
26
28
|
Load these when the user's request matches the keywords above:
|
|
27
29
|
|
|
28
30
|
- For lead magnet campaigns (comment-to-DM, resource delivery, scheduled runs), see [Lead Magnet](sub/lead-magnet.md)
|
|
31
|
+
- For autonomous lead generation (find leads, qualify, grow database), see [Lead Gen](sub/lead-gen.md)
|
|
32
|
+
- For personalized outreach (connect, DM, follow up, adapt), see [Outreach](sub/outreach.md)
|
|
29
33
|
- For SDK method signatures and parameters when writing scripts, see [SDK Reference](sdk-reference.md)
|
|
30
34
|
|
|
31
35
|
## Installation
|
|
@@ -67,8 +71,18 @@ The SDK auto-reads `BEREACH_API_KEY` from the environment. NEVER hardcode tokens
|
|
|
67
71
|
|
|
68
72
|
1. **Token** — read `BEREACH_API_KEY` from environment with no default/fallback value. If not set, stop immediately.
|
|
69
73
|
2. **Read all sub-skills** — fetch every sub-skill listed above.
|
|
70
|
-
3. **
|
|
71
|
-
|
|
74
|
+
3. **Context acquisition** — build a rich picture of the user before any workflow. This context feeds all sub-skills and makes the agent smarter from the start.
|
|
75
|
+
- **Profile + language** — `getLinkedInProfile` (0 credits). Detect language from location, headline, posts. This becomes the default for all generated content.
|
|
76
|
+
- **Recent posts** — `getPosts({ count: 10 })` (0 credits). What topics resonate, tone, offer, engagement patterns.
|
|
77
|
+
- **Followers** — `getFollowers({ count: 50 })` (1 credit/page). Reveals audience type; followers are warm leads already.
|
|
78
|
+
- **Website** — if the profile contains a website URL, fetch and extract: what they sell, who they target, positioning.
|
|
79
|
+
- **Documents/data** — if the user has provided files (ICP docs, prospect lists, CRM exports, target account lists), read them. No format requirement.
|
|
80
|
+
- **Existing campaigns** — `getStats` per known campaign (0 credits). What's running, what worked, conversions.
|
|
81
|
+
- **Lead-gen state** — `contacts.listCampaigns({ type: "lead_gen" })` (0 credits) to see existing lead-gen campaigns. `agentState.get("lead-gen")` (0 credits) to load previous state (angles, watchlist, learning data). If state exists, the agent resumes where it left off.
|
|
82
|
+
- **Analytics** — `linkedinAnalytics.getFollowerAnalytics()` (1 credit). Follower demographics and growth trends. `linkedinAnalytics.getProfileViews()` (1 credit). Who's viewing the profile — warm leads.
|
|
83
|
+
- **Company pages** — `companyPages.list()` (1 credit). If the user administers company pages, discover them for company-level posting and analytics.
|
|
84
|
+
- **Competitors** — infer from industry, profile, and posts. Note competitor names and profile URLs for the engagement watchlist.
|
|
85
|
+
4. **Welcome** — personalized welcome using profile data and context. Suggest a first action based on their profile, posts, and industry.
|
|
72
86
|
|
|
73
87
|
## Rules
|
|
74
88
|
|
|
@@ -81,11 +95,13 @@ The SDK auto-reads `BEREACH_API_KEY` from the environment. NEVER hardcode tokens
|
|
|
81
95
|
|
|
82
96
|
- Lead magnet scripts use the SDK. Tools are for interactive agent use only.
|
|
83
97
|
- Import from the `bereach` SDK exclusively. If a method doesn't exist, the operation doesn't exist.
|
|
84
|
-
- Scripts MUST be TypeScript (`.ts`). Load the [SDK Reference](sdk-reference.md) sub-skill for method signatures.
|
|
98
|
+
- Scripts MUST be TypeScript (`.ts`). Load the [SDK Reference](sdk-reference.md) sub-skill for method signatures and type imports.
|
|
99
|
+
- NEVER use `any` or untyped variables. Import types from `bereach/models/operations` (see SDK Reference "Types" section). Using `any` defeats TypeScript — bugs like `msg.body` vs `msg.text` become invisible to the compiler.
|
|
100
|
+
- tsconfig.json must include `"strict": true` and `"skipLibCheck": true`. `strict` enables `noImplicitAny` so TypeScript rejects untyped code. `skipLibCheck` ignores type errors inside `node_modules` (expected for published packages).
|
|
85
101
|
|
|
86
102
|
**Mandatory validation loop** — after generating or editing any script:
|
|
87
103
|
|
|
88
|
-
1. **Step 1 (blocking)** — Run `npx tsc --noEmit` in the script directory. It MUST exit 0. If type errors: fix them using the SDK Reference, then repeat. Do NOT run tsx until tsc passes.
|
|
104
|
+
1. **Step 1 (blocking)** — Run `npx tsc --noEmit` in the script directory. It MUST exit 0. If type errors: fix them using the SDK Reference, then repeat. Do NOT run tsx until tsc passes. This check is only meaningful when real types are used — with `any` everywhere, `tsc` validates nothing.
|
|
89
105
|
2. **Step 2** — Run `npx tsx <script-name>.ts`. Fix runtime errors (e.g. "visitProfile is not a function"). If config missing, run with minimal env to reach first SDK call.
|
|
90
106
|
3. **Both must succeed** — tsx passing alone is NOT sufficient. Do not consider the script ready until BOTH tsc and tsx succeed.
|
|
91
107
|
|
|
@@ -99,12 +115,19 @@ The SDK auto-reads `BEREACH_API_KEY` from the environment. NEVER hardcode tokens
|
|
|
99
115
|
|
|
100
116
|
| Resource | Methods |
|
|
101
117
|
| --- | --- |
|
|
102
|
-
| `client.linkedinScrapers` | `collectLikes`, `collectComments`, `collectCommentReplies`, `collectPosts`, `visitProfile`, `visitCompany` |
|
|
103
|
-
| `client.linkedinActions` | `connectProfile`, `listInvitations`, `acceptInvitation`, `sendMessage`, `replyToComment`, `likeComment`, `publishPost` |
|
|
104
|
-
| `client.linkedinChat` | `listConversations`, `searchConversations`, `findConversation`, `getMessages` |
|
|
118
|
+
| `client.linkedinScrapers` | `collectLikes`, `collectComments`, `collectCommentReplies`, `collectPosts`, `visitProfile`, `visitCompany`, `collectHashtagPosts`, `collectSavedPosts` |
|
|
119
|
+
| `client.linkedinActions` | `connectProfile`, `listInvitations`, `acceptInvitation`, `sendMessage`, `replyToComment`, `likeComment`, `publishPost`, `commentOnPost`, `likePost`, `unlikePost`, `unlikeComment`, `editComment`, `editPost`, `repostPost`, `savePost`, `unsavePost`, `followProfile`, `unfollowProfile`, `followCompany`, `unfollowCompany`, `declineInvitation`, `withdrawInvitation`, `listSentInvitations` |
|
|
120
|
+
| `client.linkedinChat` | `listConversations`, `searchConversations`, `findConversation`, `getMessages`, `archiveConversation`, `unarchiveConversation`, `starConversation`, `unstarConversation`, `listArchivedConversations`, `listStarredConversations`, `markAllRead`, `markSeen`, `reactToMessage`, `unreactToMessage`, `getUnreadCount`, `sendTypingIndicator` |
|
|
105
121
|
| `client.linkedinSearch` | `unifiedSearch`, `searchPosts`, `searchPeople`, `searchCompanies`, `searchJobs`, `searchByUrl`, `resolveParameters` |
|
|
106
|
-
| `client.profile` | `getLinkedInProfile`, `refresh`, `getPosts`, `getFollowers`, `getLimits`, `getCredits` |
|
|
122
|
+
| `client.profile` | `getLinkedInProfile`, `refresh`, `getPosts`, `getFollowers`, `getLimits`, `getCredits`, `listAccounts`, `updateAccount`, `getActivity`, `revalidate` |
|
|
107
123
|
| `client.campaigns` | `getStatus`, `syncActions`, `getStats` |
|
|
124
|
+
| `client.contacts` | `createCampaign`, `listCampaigns`, `addContacts`, `listContacts`, `updateContact`, `searchContacts`, `getStats`, `logActivity`, `getActivities` |
|
|
125
|
+
| `client.linkedinAnalytics` | `getFollowerAnalytics`, `getPostAnalytics`, `getProfileViews`, `getSearchAppearances` |
|
|
126
|
+
| `client.linkedinFeed` | `getFeed` |
|
|
127
|
+
| `client.salesNavigator` | `search`, `searchPeople`, `searchCompanies` |
|
|
128
|
+
| `client.companyPages` | `list`, `getAnalytics`, `getPermissions`, `getPosts` |
|
|
129
|
+
| `client.settings` | `getDmPolling`, `updateDmPolling` |
|
|
130
|
+
| `client.agentState` | `get`, `set` |
|
|
108
131
|
|
|
109
132
|
## Tone
|
|
110
133
|
|
|
@@ -115,7 +138,7 @@ Adapt to user's tone from their posts and messages. Comment replies: 3-5 words,
|
|
|
115
138
|
These are technical constraints BeReach requires. Everything else, adapt as needed.
|
|
116
139
|
|
|
117
140
|
1. **Dedup** — pass `campaignSlug` on every action. BeReach deduplicates by target automatically. Duplicates return `duplicate: true` and cost nothing. Pre-check: use `client.campaigns.getStatus()` or the `getStatus` tool.
|
|
118
|
-
2. **Pacing** — after every SDK/tool call, sleep a random delay. Write actions (DM, reply, like, connect, accept, sync): `random delay 8-12s`. Read actions (visit, scrape, find, count-0 checks): `random delay 2-6s`. On 429: wait the number of seconds from the error response, retry (max 3). If daily/weekly cap hit, switch action type.
|
|
141
|
+
2. **Pacing** — after every SDK/tool call, sleep a random delay. Write actions (DM, reply, like, comment, connect, accept, follow, repost, edit, sync): `random delay 8-12s`. Read actions (visit, scrape, search, find, feed, analytics, count-0 checks): `random delay 2-6s`. Inbox management (archive, star, mark-read, react, typing): `random delay 1-3s`. On 429: wait the number of seconds from the error response, retry (max 3). If daily/weekly cap hit, switch action type.
|
|
119
142
|
3. **Save incrementally** — persist tracking after each action, not at the end.
|
|
120
143
|
4. **Limits check** — call `getLimits` once per day at session start.
|
|
121
144
|
5. **Visit before connecting** — looks natural to LinkedIn.
|
|
@@ -136,8 +159,8 @@ These are technical constraints BeReach requires. Everything else, adapt as need
|
|
|
136
159
|
Each workflow is detailed in its sub-skill. Load the relevant sub-skill when needed.
|
|
137
160
|
|
|
138
161
|
- **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
|
|
139
|
-
|
|
140
|
-
|
|
162
|
+
- **Lead Gen** — autonomous lead generation. Find and qualify leads daily through multi-angle search, engagement scraping, and a learning loop. BeReach campaigns as the contacts DB. State at `~/.bereach/lead-gen-state.json`. → Lead Gen sub-skill
|
|
163
|
+
- **Outreach** — personalized LinkedIn outreach. Connect, message, follow up, adapt. Uses emergent planning (no DAG workflows). State via `agentState.get("outreach")`. → Outreach sub-skill
|
|
141
164
|
|
|
142
165
|
### Cron
|
|
143
166
|
|
|
@@ -151,7 +174,19 @@ Crons are OpenClaw scheduled tasks. Create entries in `~/.openclaw/cron/jobs.jso
|
|
|
151
174
|
{ "id": "lm-connections", "every": "1h", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run lm-connections.ts and report recap" }
|
|
152
175
|
```
|
|
153
176
|
|
|
154
|
-
|
|
177
|
+
**Lead Gen** — 1 daily cron. The spawned agent uses tools directly (no pre-generated script):
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{ "id": "lead-gen", "every": "1d", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run daily lead gen using sub/lead-gen.md playbook. Load state via agentState.get('lead-gen'), run priority channels, qualify, store, report recap." }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Outreach** — 1 daily cron. Uses emergent planning (no workflows):
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{ "id": "outreach", "every": "1d", "skill": "bereach", "sessionTarget": "spawn", "prompt": "Run daily outreach using sub/outreach.md playbook. Load state via agentState.get('outreach'). Process replies first, then accepted connections, then follow-ups, then new connections. Respect daily limits. Update state and recap." }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Always use `"sessionTarget": "spawn"` for crons that execute scripts or tool-based workflows. This spawns a sub-agent in an isolated session — it 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.
|
|
155
190
|
|
|
156
191
|
**Update check** — detect-only, never auto-upgrade:
|
|
157
192
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: openclaw-optimization
|
|
3
3
|
description: "Optimize OpenClaw for cost efficiency with Anthropic Claude. Use when: reducing AI costs, configuring model routing, configuring heartbeat, optimizing workspace files."
|
|
4
|
-
lastUpdatedAt:
|
|
4
|
+
lastUpdatedAt: 1773274027
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# OpenClaw Optimization
|
|
@@ -48,17 +48,6 @@ If you forget step 3, every subsequent message costs 5x more for no benefit. The
|
|
|
48
48
|
|
|
49
49
|
Create these in `~/.openclaw/workspace/`. Keep them lean — every token is sent on every request.
|
|
50
50
|
|
|
51
|
-
### HEARTBEAT.md
|
|
52
|
-
|
|
53
|
-
OpenClaw reads this file on every heartbeat. If nothing needs attention, the agent replies `HEARTBEAT_OK` and the message is suppressed.
|
|
54
|
-
|
|
55
|
-
```markdown
|
|
56
|
-
# Heartbeat checklist
|
|
57
|
-
|
|
58
|
-
- Check BeReach campaigns: new comments to process, pending connections to accept, messages to send.
|
|
59
|
-
- Only report if there's something actionable.
|
|
60
|
-
```
|
|
61
|
-
|
|
62
51
|
### SOUL.md
|
|
63
52
|
|
|
64
53
|
```markdown
|