bereach-openclaw 0.3.5 → 1.3.1

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
@@ -1,6 +1,6 @@
1
1
  # BeReach — OpenClaw Plugin
2
2
 
3
- LinkedIn outreach automation via [BeReach](https://berea.ch). Registers 33 in-process tools and auto-reply commands.
3
+ LinkedIn outreach automation via [BeReach](https://berea.ch). Registers 60+ in-process tools and instant status commands.
4
4
 
5
5
  ## Install
6
6
 
@@ -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,29 +119,29 @@ 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()`.
137
137
 
138
138
  ## Usage
139
139
 
140
- ### Tools (33 registered)
140
+ ### Tools (60+ registered)
141
141
 
142
142
  All BeReach operations are available as agent tools — the agent uses them automatically based on your requests. No MCP needed; tools run in-process via the `bereach` SDK.
143
143
 
144
- ### Auto-reply commands
144
+ ### Instant status commands
145
145
 
146
146
  These execute instantly without invoking the AI:
147
147
 
@@ -161,8 +161,8 @@ openclaw bereach status
161
161
 
162
162
  | Component | Description |
163
163
  | --- | --- |
164
- | `src/tools/` | 33 tool definitions generated from OpenAPI |
165
- | `src/commands/` | Auto-reply commands + CLI |
164
+ | `src/tools/` | 60+ tool definitions generated from OpenAPI |
165
+ | `src/commands/` | Instant status commands + CLI |
166
166
  | `skills/bereach/SKILL.md` | Main behavioral skill |
167
167
  | `skills/bereach/sub/lead-magnet.md` | Lead magnet workflow |
168
168
  | `skills/bereach/sdk-reference.md` | SDK method reference |
@@ -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,8 +1,8 @@
1
1
  {
2
- "id": "bereach",
2
+ "id": "bereach-openclaw",
3
3
  "name": "BeReach",
4
- "version": "0.3.5",
5
- "description": "LinkedIn outreach automation — 33 tools, auto-reply commands",
4
+ "version": "1.3.1",
5
+ "description": "LinkedIn outreach automation — 60+ tools, instant status commands",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bereach-openclaw",
3
- "version": "0.3.5",
3
+ "version": "1.3.1",
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.2.1"
18
+ "bereach": "^1.3.2"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^22.19.3",