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 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
- linkedinChat: {
41
+ chat: {
39
42
  findConversation: vi.fn(),
40
- ...overrides.linkedinChat,
43
+ ...overrides.chat,
41
44
  },
42
45
  campaigns: {
43
- syncActions: vi.fn(),
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 when previousTotal differs from current total", async () => {
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: "new" }], { total: 55, start: 0, hasMore: false }),
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.linkedinScrapers.visitProfile);
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.linkedinScrapers.visitProfile);
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.linkedinScrapers.visitProfile);
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 linkedinScrapers.visitProfile when knownDistance is null", async () => {
424
+ it("calls scrapers.visitProfile when knownDistance is null", async () => {
380
425
  const client = createMockClient();
381
- const mock = vi.mocked(client.linkedinScrapers.visitProfile);
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.linkedinScrapers.visitProfile);
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.linkedinScrapers.visitProfile);
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.linkedinScrapers.visitProfile);
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.linkedinChat.findConversation);
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.linkedinChat.findConversation);
492
- const syncMock = vi.mocked(client.campaigns.syncActions);
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.linkedinChat.findConversation);
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.linkedinChat.findConversation);
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.linkedinChat.findConversation);
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 syncActions fails after finding link", async () => {
641
+ it("still returns skip=true when sync fails after finding link", async () => {
597
642
  const client = createMockClient();
598
- const findMock = vi.mocked(client.linkedinChat.findConversation);
599
- const syncMock = vi.mocked(client.campaigns.syncActions);
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.linkedinChat.findConversation);
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.linkedinChat.findConversation);
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.linkedinChat.findConversation);
666
- const syncMock = vi.mocked(client.campaigns.syncActions);
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,
@@ -1,7 +1,7 @@
1
1
  {
2
- "id": "bereach",
2
+ "id": "bereach-openclaw",
3
3
  "name": "BeReach",
4
- "version": "0.3.4",
4
+ "version": "1.2.0",
5
5
  "description": "LinkedIn outreach automation — 33 tools, auto-reply commands",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bereach-openclaw",
3
- "version": "0.3.4",
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": "^0.1.4"
18
+ "bereach": "^1.2.3"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^22.19.3",
@@ -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: 1772724697
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 | 1772724697 |
21
- | OpenClaw Optimization | openclaw, model, opus, sonnet, haiku, config, SOUL.md, heartbeat, prompt caching, AI cost reduction, /model | openclaw-optimization.md | 1772619338 |
22
- | SDK Reference | sdk, method, parameter, signature, reference, api, script | sdk-reference.md | 1772672714 |
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. **Profile + language** — call `getLinkedInProfile` (tool or `client.profile.getLinkedInProfile()`). Profile inputs: URL (e.g. `https://linkedin.com/in/username`) or URN (`urn:li:fsd_profile:ACoAA...`). Detect user's language from profile (location, headline, posts). This becomes the default language for all generated content. Per prospect, adapt if their profile clearly indicates a different language.
71
- 4. **Welcome** — personalized welcome using profile data. Suggest a first action based on their profile.
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
- More workflows coming soon. You can build your own using the SDK methods and tools listed above.
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
- 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.
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: 1772619338
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