bereach-openclaw 1.6.2 → 1.6.4
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/node_modules/@bereach/tools/src/definitions.ts +6 -5
- package/node_modules/@bereach/tools/src/enforcement-types.ts +0 -4
- package/openclaw.plugin.json +1 -9
- package/package.json +2 -5
- package/skills/bereach/SKILL.md +4 -4
- package/skills/bereach/sub/lead-gen.md +12 -10
- package/skills/bereach/sub/lead-magnet.md +7 -7
- package/skills/bereach/sub/outreach.md +21 -4
- package/skills/bereach/workspace/soul-template.md +13 -3
- package/src/commands/setup.ts +0 -61
- package/src/hooks/context/formatters.ts +1 -1
- package/src/hooks/context/index.ts +3 -7
- package/src/hooks/context/task-context.ts +92 -2
- package/src/hooks/detect-task-mode.ts +1 -13
- package/src/hooks/types.ts +2 -2
- package/src/index.ts +1 -72
- package/src/soul-template-content.ts +2 -2
- package/src/tools/index.ts +3 -17
|
@@ -15,7 +15,7 @@ export const definitions: ToolDefinition[] = [
|
|
|
15
15
|
|
|
16
16
|
{
|
|
17
17
|
name: "bereach_collect_likes",
|
|
18
|
-
description: "Scrape LinkedIn post likes. Returns paginated list of profiles who liked a post. Auto-creates a contact record for each liker.
|
|
18
|
+
description: "Scrape LinkedIn post likes. Returns paginated list of profiles who liked a post. Auto-creates a contact record for each liker. **campaignSlug is MANDATORY whenever a campaign is active** — without it, likers are orphaned at user level and never enter the campaign pipeline.",
|
|
19
19
|
handler: "scrapers.collectLikes",
|
|
20
20
|
apiPath: "/collect/linkedin/likes",
|
|
21
21
|
apiMethod: "POST",
|
|
@@ -33,7 +33,7 @@ export const definitions: ToolDefinition[] = [
|
|
|
33
33
|
|
|
34
34
|
{
|
|
35
35
|
name: "bereach_collect_comments",
|
|
36
|
-
description: "Scrape LinkedIn post comments. Returns paginated list of commenters with comment text and URNs. Auto-creates a contact record for each commenter.",
|
|
36
|
+
description: "Scrape LinkedIn post comments. Returns paginated list of commenters with comment text and URNs. Auto-creates a contact record for each commenter. **campaignSlug is MANDATORY whenever a campaign is active** — without it, commenters are orphaned at user level and never enter the campaign pipeline.",
|
|
37
37
|
handler: "scrapers.collectComments",
|
|
38
38
|
apiPath: "/collect/linkedin/comments",
|
|
39
39
|
apiMethod: "POST",
|
|
@@ -177,7 +177,7 @@ export const definitions: ToolDefinition[] = [
|
|
|
177
177
|
|
|
178
178
|
{
|
|
179
179
|
name: "bereach_collect_hashtag_posts",
|
|
180
|
-
description: "Scrape LinkedIn posts for a given hashtag. Lead-gen channel for topical prospecting. 1 credit. Auto-creates a contact for each post author.",
|
|
180
|
+
description: "Scrape LinkedIn posts for a given hashtag. Lead-gen channel for topical prospecting. 1 credit. Auto-creates a contact for each post author. **campaignSlug is MANDATORY whenever a campaign is active** — without it, authors are orphaned at user level and never enter the campaign pipeline.",
|
|
181
181
|
handler: "scrapers.collectHashtagPosts",
|
|
182
182
|
apiPath: "/collect/linkedin/hashtag",
|
|
183
183
|
apiMethod: "POST",
|
|
@@ -211,7 +211,7 @@ export const definitions: ToolDefinition[] = [
|
|
|
211
211
|
|
|
212
212
|
{
|
|
213
213
|
name: "bereach_unified_search",
|
|
214
|
-
description: "Unified LinkedIn Search — search posts, people, companies, or jobs with a single endpoint. Supports all filter types via category selection.
|
|
214
|
+
description: "Unified LinkedIn Search — search posts, people, companies, or jobs with a single endpoint. Supports all filter types via category selection. **campaignSlug is MANDATORY whenever a campaign is active or the user asked you to add results to a pipeline** — without it, discovered contacts are orphaned at user level and never appear in the campaign.",
|
|
215
215
|
handler: "search.search",
|
|
216
216
|
apiPath: "/search/linkedin",
|
|
217
217
|
apiMethod: "POST",
|
|
@@ -1876,7 +1876,7 @@ export const definitions: ToolDefinition[] = [
|
|
|
1876
1876
|
|
|
1877
1877
|
{
|
|
1878
1878
|
name: "bereach_search_sales_nav",
|
|
1879
|
-
description: "Unified Sales Navigator search — people and companies. Requires Sales Navigator subscription. Credits = floor(results / 10), min 1.",
|
|
1879
|
+
description: "Unified Sales Navigator search — people and companies. Requires Sales Navigator subscription. Credits = floor(results / 10), min 1. **campaignSlug is MANDATORY whenever a campaign is active** — without it, discovered contacts are orphaned at user level and never enter the campaign pipeline.",
|
|
1880
1880
|
handler: "salesNav.search",
|
|
1881
1881
|
apiPath: "/search/linkedin/sales-nav",
|
|
1882
1882
|
apiMethod: "POST",
|
|
@@ -1902,6 +1902,7 @@ export const definitions: ToolDefinition[] = [
|
|
|
1902
1902
|
school: { type: "array", items: { type: "string" }, description: "School name IDs (use bereach_resolve_parameters with type=SCHOOL to get IDs)." },
|
|
1903
1903
|
start: { type: "integer", minimum: 0, description: "Pagination offset." },
|
|
1904
1904
|
count: { type: "integer", minimum: 1, maximum: 25, description: "Results per page (max 25)." },
|
|
1905
|
+
campaignSlug: { type: "string", description: "Campaign ID. Auto-adds discovered contacts to this campaign. MANDATORY whenever a campaign is active." },
|
|
1905
1906
|
},
|
|
1906
1907
|
},
|
|
1907
1908
|
},
|
|
@@ -198,10 +198,6 @@ export interface PluginConfig {
|
|
|
198
198
|
writePacingMax?: number;
|
|
199
199
|
/** Minimum minutes between direct DM sends (default 5). Override via dm_pacing_minutes context. */
|
|
200
200
|
dmPacingMinutes?: number;
|
|
201
|
-
/** Gateway URL for webhook execution */
|
|
202
|
-
gatewayUrl?: string;
|
|
203
|
-
/** Gateway hooks auth token */
|
|
204
|
-
hooksToken?: string;
|
|
205
201
|
}
|
|
206
202
|
|
|
207
203
|
// ---------------------------------------------------------------------------
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "bereach-openclaw",
|
|
3
3
|
"name": "BeReach",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.4",
|
|
5
5
|
"description": "LinkedIn outreach automation — 75+ tools, hook-based enforcement, dynamic context",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
@@ -66,14 +66,6 @@
|
|
|
66
66
|
"maximum": 30,
|
|
67
67
|
"default": 10,
|
|
68
68
|
"description": "Maximum delay (seconds) before write operations (default: 10s)"
|
|
69
|
-
},
|
|
70
|
-
"gatewayUrl": {
|
|
71
|
-
"type": "string",
|
|
72
|
-
"description": "Gateway URL for webhook execution (default: http://localhost:18789)"
|
|
73
|
-
},
|
|
74
|
-
"hooksToken": {
|
|
75
|
-
"type": "string",
|
|
76
|
-
"description": "Gateway hooks auth token (set by /bereach setup)"
|
|
77
69
|
}
|
|
78
70
|
}
|
|
79
71
|
},
|
package/package.json
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bereach-openclaw",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "BeReach LinkedIn automation plugin for OpenClaw",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts"
|
|
8
8
|
},
|
|
9
|
-
"bin": {
|
|
10
|
-
"bereach-openclaw": "./src/connector-cli.ts"
|
|
11
|
-
},
|
|
12
9
|
"files": [
|
|
13
10
|
"src/",
|
|
14
11
|
"skills/",
|
|
@@ -51,7 +48,7 @@
|
|
|
51
48
|
},
|
|
52
49
|
"devDependencies": {
|
|
53
50
|
"@playwright/test": "^1.59.1",
|
|
54
|
-
"@types/node": "^25.
|
|
51
|
+
"@types/node": "^25.6.0",
|
|
55
52
|
"@upstash/box": "^0.1.32",
|
|
56
53
|
"tsx": "^4.21.0",
|
|
57
54
|
"typescript": "^6.0.2",
|
package/skills/bereach/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bereach
|
|
3
3
|
description: "Automate LinkedIn outreach via BeReach (bereach.ai). 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:
|
|
4
|
+
lastUpdatedAt: 1775933897
|
|
5
5
|
metadata: { "openclaw": { "requires": { "env": ["BEREACH_API_KEY"] }, "primaryEnv": "BEREACH_API_KEY" } }
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -22,9 +22,9 @@ Load sub-skills **on-demand** when the user's request matches a workflow.
|
|
|
22
22
|
|
|
23
23
|
| Sub-skill | Keywords | URL | lastUpdatedAt |
|
|
24
24
|
| ------------- | -------- | --- | ------------- |
|
|
25
|
-
| Lead Gen | lead gen, find leads, search, qualify, ICP, pipeline, scrape, competitor, prospecting, hashtag, Sales Navigator | sub/lead-gen.md |
|
|
26
|
-
| Outreach | outreach, connect, DM, message, follow up, connection request, reply, warming, draft, batch | sub/outreach.md |
|
|
27
|
-
| Engagement | engagement, comment warming, accept invitations, connection requests, engage-comment, connect-review, connect-send | sub/lead-magnet.md |
|
|
25
|
+
| Lead Gen | lead gen, find leads, search, qualify, ICP, pipeline, scrape, competitor, prospecting, hashtag, Sales Navigator | sub/lead-gen.md | 1775932100 |
|
|
26
|
+
| Outreach | outreach, connect, DM, message, follow up, connection request, reply, warming, draft, batch | sub/outreach.md | 1775933291 |
|
|
27
|
+
| Engagement | engagement, comment warming, accept invitations, connection requests, engage-comment, connect-review, connect-send | sub/lead-magnet.md | 1775923140 |
|
|
28
28
|
| Warmup | warmup, warm up, account warmup, engagement, likes, visibility, ramp up, pre-warming | sub/warmup.md | 1775908473 |
|
|
29
29
|
| Content | content, post, publish, LinkedIn post, content strategy, draft, article, thought leadership | sub/content.md | 1775908473 |
|
|
30
30
|
| Inbox | inbox, triage, classify, archive, star, respond, unread, conversation, spam, inbox management | sub/inbox.md | 1775908473 |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bereach-lead-gen
|
|
3
3
|
description: "Lead generation - discover leads via search/scrape (batch), qualify one contact at a time (unit)."
|
|
4
|
-
lastUpdatedAt:
|
|
4
|
+
lastUpdatedAt: 1775932100
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -30,10 +30,10 @@ One agent call searches/scrapes and adds multiple profiles.
|
|
|
30
30
|
1. **Load state**: `bereach_state_get({ key: "{campaignId}_lead-gen" })` — read `processedSources`, `learningNotes`, `resolvedParams`
|
|
31
31
|
2. **Resolve parameters** (if not cached): `bereach_resolve_parameters` for GEO, INDUSTRY, COMPANY_SIZE. Cache in state.
|
|
32
32
|
3. **Pick 1-2 channels** based on `learningNotes`. See Channels below.
|
|
33
|
-
4. **
|
|
33
|
+
4. **Always pass `campaignSlug`** on every search/collect call. Profiles are auto-added to the campaign — do NOT call `bereach_contacts_upsert` or `bereach_contacts_add` on fresh results. Manual upsert is only for pre-existing URLs typed by the user.
|
|
34
34
|
5. **Save state**: update `processedSources` and `learningNotes`.
|
|
35
35
|
|
|
36
|
-
Rules: do NOT visit profiles, send messages, or connect. Discovery only.
|
|
36
|
+
Rules: do NOT visit profiles, send messages, or connect. Discovery only. Every discovery tool call MUST include `campaignSlug` when running inside a task.
|
|
37
37
|
|
|
38
38
|
## Filter & Visit (Batch Mode)
|
|
39
39
|
|
|
@@ -67,7 +67,7 @@ Deep ICP scoring on leads. Profile data already populated from visit step - no v
|
|
|
67
67
|
Read full profile (about, posts, positions, connections). Score against full ICP (activity level, company stage, seniority, engagement).
|
|
68
68
|
- Match: `lifecycleStage: "qualified"`, assign `hotScore` (0-100)
|
|
69
69
|
- No match: `lifecycleStage: "rejected"`, `hotScore: 0`
|
|
70
|
-
- `bereach_contacts_log_activity({ contactId, type: "qualification",
|
|
70
|
+
- `bereach_contacts_log_activity({ contactId, activities: [{ type: "qualification", content: "specific reason" }] })`
|
|
71
71
|
|
|
72
72
|
### TaskResult
|
|
73
73
|
```json
|
|
@@ -78,14 +78,16 @@ Read full profile (about, posts, positions, connections). Score against full ICP
|
|
|
78
78
|
|
|
79
79
|
Ordered by typical warmth. Pick based on ICP and `learningNotes`.
|
|
80
80
|
|
|
81
|
+
All calls below MUST include `campaignSlug` so results auto-add to the active campaign.
|
|
82
|
+
|
|
81
83
|
| # | Channel | Tool | Notes |
|
|
82
84
|
|---|---------|------|-------|
|
|
83
|
-
| 1 | Engagement scraping | `bereach_collect_comments` / `bereach_collect_likes` on competitor posts | Best ratio: 1 credit = up to 100 profiles. Use `count=0` (free) to check volume first. |
|
|
84
|
-
| 2 | Content search | `bereach_unified_search({ category: "posts" })` | Find people posting about pain points. Boolean syntax supported. |
|
|
85
|
-
| 3 | People search | `bereach_unified_search({ category: "people" })` | Filters: title, location, industry, companySize. Requires `bereach_resolve_parameters`. |
|
|
86
|
-
| 4 | Sales Navigator | `bereach_search_sales_nav` | Highest precision. Always try first; fall back to unified_search on 403. |
|
|
87
|
-
| 5 | Hashtag scraping | `bereach_collect_hashtag_posts` | Industry hashtag authors = leads. |
|
|
88
|
-
| 6 | Job search | `bereach_unified_search({ category: "jobs" })` | Hiring = buying signal. Find company, then search decision-makers. |
|
|
85
|
+
| 1 | Engagement scraping | `bereach_collect_comments({ postUrl, campaignSlug })` / `bereach_collect_likes({ postUrl, campaignSlug })` on competitor posts | Best ratio: 1 credit = up to 100 profiles. Use `count=0` (free) to check volume first. |
|
|
86
|
+
| 2 | Content search | `bereach_unified_search({ category: "posts", campaignSlug })` | Find people posting about pain points. Boolean syntax supported. |
|
|
87
|
+
| 3 | People search | `bereach_unified_search({ category: "people", campaignSlug })` | Filters: title, location, industry, companySize. Requires `bereach_resolve_parameters`. |
|
|
88
|
+
| 4 | Sales Navigator | `bereach_search_sales_nav({ campaignSlug, ... })` | Highest precision. Always try first; fall back to unified_search on 403. |
|
|
89
|
+
| 5 | Hashtag scraping | `bereach_collect_hashtag_posts({ hashtag, campaignSlug })` | Industry hashtag authors = leads. |
|
|
90
|
+
| 6 | Job search | `bereach_unified_search({ category: "jobs", campaignSlug })` | Hiring = buying signal. Find company, then search decision-makers. |
|
|
89
91
|
| 7 | Profile views | `bereach_get_profile_views` | Highest-intent passive signal. |
|
|
90
92
|
| 8 | Followers | `bereach_get_followers` | Already follow the user = warm. |
|
|
91
93
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bereach-engagement
|
|
3
3
|
description: "Engagement warming - comment on one contact's post (unit), accept ICP invitations (batch), connect with one contact (unit)."
|
|
4
|
-
lastUpdatedAt:
|
|
4
|
+
lastUpdatedAt: 1775923140
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -59,7 +59,7 @@ You receive one contact (contactId or URL). Read their posts and leave one genui
|
|
|
59
59
|
3. Check for prior comments: `bereach_contacts_get_activities({ contactId, type: "post_interaction", limit: 5 })`
|
|
60
60
|
4. If not already commented: compose a genuine comment (2-3 sentences)
|
|
61
61
|
5. Post: `bereach_comment_on_post({ postUrn, comment })`
|
|
62
|
-
6. Log: `bereach_contacts_log_activity({ contactId, type: "post_interaction",
|
|
62
|
+
6. Log: `bereach_contacts_log_activity({ contactId, activities: [{ type: "post_interaction", content: "Commented on post about X" }] })`
|
|
63
63
|
|
|
64
64
|
### Comment Guidelines
|
|
65
65
|
- Be genuine and add value. Reference specific points from the post.
|
|
@@ -90,10 +90,10 @@ One agent call processes all pending invitations. Fast-checking headlines is lig
|
|
|
90
90
|
2. For each invitation:
|
|
91
91
|
a. Fast check: sender's headline + company vs campaign ICP
|
|
92
92
|
b. If match:
|
|
93
|
-
- Accept: `bereach_accept_invitation({ invitationId })`
|
|
94
|
-
- Upsert contact: `bereach_contacts_upsert({ linkedinUrl, source: "invitation" })`
|
|
95
|
-
- Add to campaign: `bereach_contacts_add({
|
|
96
|
-
- Log: `bereach_contacts_log_activity({ contactId, type: "connection_accepted" })`
|
|
93
|
+
- Accept: `bereach_accept_invitation({ invitationId, sharedSecret })`
|
|
94
|
+
- Upsert contact: `bereach_contacts_upsert({ contacts: [{ linkedinUrl, name: senderName, source: "invitation" }] })`
|
|
95
|
+
- Add to campaign: `bereach_contacts_add({ campaignId, contacts: [{ linkedinUrl, name: senderName, source: "invitation" }] })`
|
|
96
|
+
- Log: `bereach_contacts_log_activity({ contactId, activities: [{ type: "connection_accepted" }] })`
|
|
97
97
|
c. If no match: skip (do NOT decline — leave for user)
|
|
98
98
|
|
|
99
99
|
### Rules
|
|
@@ -120,7 +120,7 @@ You receive one contact (contactId or URL). Visit and send a personalized connec
|
|
|
120
120
|
2. Compose personalized connection note (300 chars max). Reference something specific from their profile.
|
|
121
121
|
3. Send: `bereach_connect_profile({ profileUrl, note })`
|
|
122
122
|
4. Update: `bereach_contacts_update({ contactId, outreachStatus: "connection_sent" })`
|
|
123
|
-
5. Log: `bereach_contacts_log_activity({ contactId, type: "connection_request" })`
|
|
123
|
+
5. Log: `bereach_contacts_log_activity({ contactId, activities: [{ type: "connection_request" }] })`
|
|
124
124
|
|
|
125
125
|
### Connection Note Guidelines
|
|
126
126
|
- **HARD LIMIT: 300 characters max** (LinkedIn API rejects longer notes with a 422 error). Count characters BEFORE sending. If over 300, shorten aggressively — cut adjectives, compress phrasing, remove line breaks. A punchy 200-char note beats a verbose 300-char one.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bereach-outreach
|
|
3
3
|
description: "LinkedIn outreach - draft one message per contact (unit), handle one reply per contact (unit). No bulk loops."
|
|
4
|
-
lastUpdatedAt:
|
|
4
|
+
lastUpdatedAt: 1775933291
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!--
|
|
@@ -67,14 +67,14 @@ You receive one contact in the task prompt. Send immediately — this person is
|
|
|
67
67
|
- **do-not-contact** → log `do_not_contact` activity (auto-sets flag)
|
|
68
68
|
- **meeting request** → log `meeting_booked` activity
|
|
69
69
|
3. Send reply: `bereach_send_message({ profileUrl, message })`
|
|
70
|
-
4. Log: `bereach_contacts_log_activity({ contactId, type: "reply_received",
|
|
70
|
+
4. Log: `bereach_contacts_log_activity({ contactId, activities: [{ type: "reply_received", content: "classification + response" }] })`
|
|
71
71
|
|
|
72
72
|
### For a newly connected contact (outreachStatus: "connected")
|
|
73
73
|
1. Visit profile: `bereach_visit_profile({ profileUrl })` — 1 credit
|
|
74
74
|
2. Compose personalized icebreaker using campaign playbook
|
|
75
75
|
3. Send: `bereach_send_message({ profileUrl, message })`
|
|
76
76
|
4. Update: `bereach_contacts_update({ contactId, outreachStatus: "dm_sent" })`
|
|
77
|
-
5. Log: `bereach_contacts_log_activity({ contactId, type: "message",
|
|
77
|
+
5. Log: `bereach_contacts_log_activity({ contactId, activities: [{ type: "message", content: "post-connection icebreaker" }] })`
|
|
78
78
|
|
|
79
79
|
### Rules
|
|
80
80
|
- Send immediately — these are warm conversations.
|
|
@@ -140,6 +140,23 @@ Three modes — never confuse them:
|
|
|
140
140
|
| `meeting_booked` | → `meeting_booked` |
|
|
141
141
|
| `do_not_contact` | → `doNotContact: true` |
|
|
142
142
|
|
|
143
|
+
## Resolving a contact by name (CRITICAL)
|
|
144
|
+
|
|
145
|
+
When the user refers to a contact by name ("draft a DM for Alex", "message the CTO from yesterday", "follow up with T66 Candidate") and you do NOT already have a LinkedIn URL:
|
|
146
|
+
|
|
147
|
+
1. **Search first, ask never**: call `bereach_contacts_search({ name: "<name fragment>" })` — it matches case-insensitive substrings. NEVER ask the user for a URL before searching. The whole point is that the user shouldn't have to remember URLs.
|
|
148
|
+
2. **If 0 matches**: tell the user "no contact named X in your pipeline" and offer to search LinkedIn.
|
|
149
|
+
3. **If 1 match**: proceed with that contact — confirm by name in your reply ("Drafting for {name} — {headline}").
|
|
150
|
+
4. **If 2+ matches (ambiguous)**: present a short numbered list with distinguishing details (name, title, company, campaign) and ask which one. Do NOT pick one silently. Example:
|
|
151
|
+
> I found 3 contacts matching "Alex":
|
|
152
|
+
> 1. Alex Martin — CTO at PayDrop (Task Ambiguity Test)
|
|
153
|
+
> 2. Alex Chen — VP Engineering at Stripe (SaaS Leaders EU)
|
|
154
|
+
> 3. Alex Dupont — Founder at Kiro (Task Ambiguity Test)
|
|
155
|
+
> Which one?
|
|
156
|
+
5. **Once resolved**, use `contactId` or `linkedinUrl` from the search result — never fabricate a URL from the name.
|
|
157
|
+
|
|
158
|
+
This applies to DM drafts, follow-ups, connect requests, activity logs, and any tool that takes a contactId.
|
|
159
|
+
|
|
143
160
|
## Interactive Mode
|
|
144
161
|
|
|
145
162
|
When the user asks for outreach in chat:
|
|
@@ -153,7 +170,7 @@ When the user asks for outreach in chat:
|
|
|
153
170
|
|
|
154
171
|
When a user says "never contact this person", "blacklist them", "add to blacklist", or similar:
|
|
155
172
|
1. Find the contact: `bereach_contacts_get_by_url({ linkedinUrl })` or `bereach_contacts_search({ query })`
|
|
156
|
-
2. Set the flag: `bereach_contacts_log_activity({ contactId, type: "do_not_contact",
|
|
173
|
+
2. Set the flag: `bereach_contacts_log_activity({ contactId, activities: [{ type: "do_not_contact", content: "Blacklisted by user: {reason}" }] })`
|
|
157
174
|
|
|
158
175
|
This auto-sets `doNotContact: true` on the contact. The enforcement engine blocks ALL outreach tools (DM, connect, scheduled messages, comments, likes) for blacklisted contacts across ALL campaigns.
|
|
159
176
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
lastUpdatedAt:
|
|
2
|
+
lastUpdatedAt: 1775933897
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
<!--
|
|
@@ -37,7 +37,7 @@ Live data (credits, limits, pipeline, contexts) is injected below each turn. Don
|
|
|
37
37
|
## Rules
|
|
38
38
|
|
|
39
39
|
- **SAVE IMMEDIATELY**: when the user provides ICP, tone, playbook, or any campaign information, call `bereach_context_set` IN THE SAME TURN. Never wait for confirmation, never acknowledge without saving. If you discussed it but didn't call the tool, it's lost. This is the #1 priority rule.
|
|
40
|
-
-
|
|
40
|
+
- **campaignSlug on EVERY tool call (CRITICAL)**: if there is an active campaign (in live status) OR the user mentioned adding results to a campaign, you MUST pass `campaignSlug` on every search, scrape, visit, connect, message, comment, and like call. This both dedups AND auto-links results to the campaign (sets `outreachCampaignId`, creates `CampaignContact` rows). Skipping `campaignSlug` means your results float at user level and never enter the pipeline — the user's campaign will appear empty. No exceptions.
|
|
41
41
|
- Connection requests: 30/day. Check pendingConnection from visit response first.
|
|
42
42
|
- Language: respond in user's language. DMs: match conversation language.
|
|
43
43
|
- **NO JARGON**: see Identity section. Marketing terms (ICP, pipeline, leads, outreach) are fine.
|
|
@@ -45,13 +45,23 @@ Live data (credits, limits, pipeline, contexts) is injected below each turn. Don
|
|
|
45
45
|
- Formatting: tables for contacts (Name, Title, Company, Score). No raw IDs/URNs.
|
|
46
46
|
- Campaign naming: ALWAYS use a clear **human-readable title** when creating campaigns (e.g., "Reverse Prospecting - LinkedIn Connections", "SaaS Sales Leaders EU"). Never use slugs, kebab-case, or technical IDs. When referring to campaigns in conversation, always use the title - never the slug/ID.
|
|
47
47
|
- Links in recaps: when giving a campaign recap, status update, or summary, ALWAYS include the relevant clickable dashboard link (pipeline, context, drafts, campaigns). The URLs are provided in the "Dashboard Links" section of your live status.
|
|
48
|
-
- Auto-save: visitProfile, findConversation, collectComments, collectLikes, collectPosts, search.people and other scrape/search tools all auto-create/update contacts. Do NOT manually save profile or conversation data. Do NOT use contacts.upsert for data that was just scraped/visited.
|
|
48
|
+
- Auto-save: visitProfile, findConversation, collectComments, collectLikes, collectPosts, search.people and other scrape/search tools all auto-create/update contacts. Do NOT manually save profile or conversation data. Do NOT use contacts.upsert for data that was just scraped/visited. **Auto-link to the campaign only works if you pass `campaignSlug`** — without it, the contacts are created but left unattached.
|
|
49
49
|
- State saves: only save pipeline progress (phase, scraped sources) to agentState. Never store profile data in state.
|
|
50
50
|
- Error recovery: if a tool call fails or is blocked 3+ times in a row, STOP retrying it immediately. Move on to the next contact, try a completely different tool, or ask the user for guidance. Never loop on a failing tool - each retry costs LLM tokens with zero value.
|
|
51
51
|
- **LinkedIn URL accuracy (CRITICAL)**: NEVER fabricate URLs. Every URL must come from a tool result. No URL? Say "URL not available" or search first. Never construct from name+role.
|
|
52
|
+
- **Resolve contacts by name FIRST (CRITICAL)**: when the user refers to a contact by name only (no URL) — "draft a DM for Alex", "message John", "follow up with T66 Candidate" — your FIRST action MUST be `bereach_contacts_search({ name: "<name>" })`. NEVER ask the user for a URL before searching. 0 matches → tell the user, offer to search LinkedIn. 1 match → use it. 2+ matches → show a numbered list with (name, title, company, campaign) and ask which one. Never pick one silently, never fabricate URLs from names.
|
|
52
53
|
- **Delivery modes** — three distinct modes: **Draft** (`status:"draft"`) = review first, default for bulk. **Schedule** (`status:"scheduled"` + `scheduledSendAt`) = auto-sends at specified time. **Send now** (`status:"scheduled"`, no scheduledSendAt) = immediate. User says "draft"/"prepare" → Draft. "schedule"/"send at X" → Schedule. "send"/"reply" → Send now.
|
|
53
54
|
- Sales Navigator: try `bereach_search_sales_nav` first; fall back to `bereach_unified_search` only after 403. Past failures ≠ permanent. Search silently.
|
|
54
55
|
- Writing quality: a short, authentic message beats a long, generic one.
|
|
56
|
+
- **Copywriting base rules (ALL LinkedIn content — DMs, notes, comments, posts)**:
|
|
57
|
+
- **No em dashes** (—). Use a regular dash (-) sparingly, or rephrase. Em dashes are a top tell of AI-generated text.
|
|
58
|
+
- **Sound like a real person, not a bot**: no "I hope this message finds you well", no "I wanted to reach out", no "As a [role]". Speak the way the user would text a peer.
|
|
59
|
+
- **No emojis** unless the contact used one first in the same conversation, or the tone-voice context explicitly says emojis are fine.
|
|
60
|
+
- **No Title Case headings** in messages. Sentence case only. No markdown bold/headers inside a DM body — LinkedIn renders plain text.
|
|
61
|
+
- **No filler openers** ("Great question!", "Love your post!", "Awesome profile"). Get to the point in the first sentence.
|
|
62
|
+
- **Match the contact's register**: if they wrote 6 words in lowercase, don't reply with a formal paragraph. If they wrote formally, match it.
|
|
63
|
+
- **One idea per message**. Don't stack pitch + question + CTA + signature in a 300-char DM.
|
|
64
|
+
- **Never repeat the contact's name more than once** per message. Using it 2+ times is a salesperson tell.
|
|
55
65
|
- Per-contact isolation: when batch-processing contacts, ALWAYS call visitProfile or contacts.getByUrl for EACH contact immediately before composing their message. Never compose a message using context from a previously processed contact. One contact = one fresh lookup.
|
|
56
66
|
- **Bulk → campaign (CRITICAL)**: 6+ contacts → propose campaign, don't execute individually. Up to 5 = OK in chat. Search/discovery and bulk_visit = always OK (read-only).
|
|
57
67
|
- Context extraction: when the user provides outreach instructions, tone, or ICP criteria, ALWAYS extract and save as campaign-scoped context entries. Never lose user instructions.
|
package/src/commands/setup.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { errMsg } from "@bereach/tools/utils";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
2
|
import { API_BASE } from "../hooks/utils";
|
|
4
3
|
import { isApiBaseConfigured } from "../hooks/cache";
|
|
5
4
|
import { readEnv } from "../env";
|
|
@@ -50,66 +49,6 @@ export function registerSetupCommand(api: any) {
|
|
|
50
49
|
issues.push("API Key: NOT configured. Set BEREACH_API_KEY in plugin settings or environment.");
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
// 3. Gateway hooks configuration (auto-configure if missing)
|
|
54
|
-
let hooksConfigured = false;
|
|
55
|
-
try {
|
|
56
|
-
if (typeof api?.config?.get === "function") {
|
|
57
|
-
const hooksEnabled = api.config.get("hooks.enabled");
|
|
58
|
-
const hooksToken = api.config.get("hooks.token");
|
|
59
|
-
if (hooksEnabled && hooksToken) {
|
|
60
|
-
ok.push("Gateway Hooks: enabled (token set)");
|
|
61
|
-
hooksConfigured = true;
|
|
62
|
-
} else if (hooksEnabled) {
|
|
63
|
-
const token = randomBytes(24).toString("hex");
|
|
64
|
-
api.config.set("hooks.token", token);
|
|
65
|
-
ok.push(`Gateway Hooks: enabled (token auto-generated: ${token.slice(0, 8)}...)`);
|
|
66
|
-
hooksConfigured = true;
|
|
67
|
-
} else {
|
|
68
|
-
api.config.set("hooks.enabled", true);
|
|
69
|
-
const token = randomBytes(24).toString("hex");
|
|
70
|
-
api.config.set("hooks.token", token);
|
|
71
|
-
ok.push(`Gateway Hooks: auto-enabled (token: ${token.slice(0, 8)}...). Restart gateway to apply.`);
|
|
72
|
-
hooksConfigured = true;
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
issues.push("Gateway Hooks: cannot check (api.config not available).");
|
|
76
|
-
}
|
|
77
|
-
} catch (err) {
|
|
78
|
-
issues.push(`Gateway Hooks: error (${errMsg(err)})`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 4. Gateway webhook probe
|
|
82
|
-
if (hooksConfigured) {
|
|
83
|
-
const gatewayUrl = api?.pluginConfig?.gatewayUrl
|
|
84
|
-
?? readEnv("OPENCLAW_GATEWAY_URL")
|
|
85
|
-
?? "http://localhost:18789";
|
|
86
|
-
const hooksToken = api?.config?.get?.("hooks.token")
|
|
87
|
-
?? api?.pluginConfig?.hooksToken
|
|
88
|
-
?? readEnv("OPENCLAW_HOOKS_TOKEN");
|
|
89
|
-
|
|
90
|
-
if (hooksToken) {
|
|
91
|
-
try {
|
|
92
|
-
const res = await fetch(`${gatewayUrl}/hooks/agent`, {
|
|
93
|
-
method: "POST",
|
|
94
|
-
headers: {
|
|
95
|
-
Authorization: `Bearer ${hooksToken}`,
|
|
96
|
-
"Content-Type": "application/json",
|
|
97
|
-
},
|
|
98
|
-
body: JSON.stringify({ message: "ping", model: "echo" }),
|
|
99
|
-
signal: AbortSignal.timeout(5000),
|
|
100
|
-
});
|
|
101
|
-
if (res.status === 401 || res.status === 403) {
|
|
102
|
-
issues.push(`Webhook probe: auth rejected (HTTP ${res.status}). Token mismatch between plugin and gateway config.`);
|
|
103
|
-
} else {
|
|
104
|
-
ok.push(`Webhook endpoint: reachable at ${gatewayUrl}`);
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
// Gateway not up yet is OK during initial setup
|
|
108
|
-
ok.push(`Webhook endpoint: ${gatewayUrl} (not reachable yet - gateway may need restart)`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
52
|
// Build output
|
|
114
53
|
const lines: string[] = [
|
|
115
54
|
"BeReach Setup",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* These functions take data and return strings — no side effects, no API calls.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { cacheSet, type CacheStore, type
|
|
6
|
+
import { cacheSet, type CacheStore, type OnboardingState, type RecentEvent } from "../cache";
|
|
7
7
|
import { type DbCampaign, type SessionState } from "../types";
|
|
8
8
|
import { errMsg, createLogger, CHAT_BASE, PRICING_URL, apiFetch } from "../utils";
|
|
9
9
|
import { readEnv } from "../../env";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { getOrFetch, sessionStart, type CacheStore, type ContextEntry } from "../cache";
|
|
10
10
|
import { SOUL_TEMPLATE } from "../../soul-template-content";
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
import { type SessionState, detectTaskMode } from "../types";
|
|
13
13
|
import {
|
|
14
14
|
errMsg,
|
|
@@ -237,10 +237,8 @@ export function registerContextHook(api: any, apiKey: string | undefined, state:
|
|
|
237
237
|
const metadata: Record<string, unknown> = { ...(promptCtx?.metadata ?? {}) };
|
|
238
238
|
|
|
239
239
|
// Parse [TASK_META: ...] from the prompt message (used by Docker test runner
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
// env-var path always takes priority. In production chat, users don't type TASK_META.
|
|
243
|
-
if (!metadata.taskType && !readEnv("BEREACH_TASK_TYPE")) {
|
|
240
|
+
// to pass task metadata). In production chat, users don't type TASK_META.
|
|
241
|
+
if (!metadata.taskType) {
|
|
244
242
|
const msgs = Array.isArray(promptCtx?.messages) ? promptCtx.messages : [];
|
|
245
243
|
const lastUserMsg = msgs.filter((m: any) => m?.role === "user").pop();
|
|
246
244
|
const msgText = promptCtx?.prompt
|
|
@@ -329,5 +327,3 @@ export function registerContextHook(api: any, apiKey: string | undefined, state:
|
|
|
329
327
|
export const _checkBusinessHours = checkBusinessHours;
|
|
330
328
|
/** @internal — exported for testing only */
|
|
331
329
|
export { rankCampaigns as _rankCampaigns } from "./formatters";
|
|
332
|
-
/** @internal — exported for testing only */
|
|
333
|
-
export const _formatAnthropicKeyWarning = formatAnthropicKeyWarning;
|
|
@@ -68,9 +68,99 @@ export function buildTaskContext(taskMode: TaskModeInfo, data: CacheStore): stri
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
lines.push("### Execution Protocol (MANDATORY)");
|
|
72
|
+
lines.push("You are NOT in chat mode. You are an automated worker. You MUST:");
|
|
73
|
+
lines.push("1. Call the BeReach tools to perform the task. You have a pruned toolset — only the tools needed for this task type are available.");
|
|
74
|
+
lines.push("2. Base your JSON result EXCLUSIVELY on real tool responses. Do NOT invent contacts, URLs, names, counts, or outcomes.");
|
|
75
|
+
lines.push("3. If a required tool returns an error or empty result, STOP and report `{ \"success\": false, \"error\": \"<what failed>\" }` — do NOT fabricate a success.");
|
|
76
|
+
lines.push("4. Do NOT ask the user anything. Do NOT write chat prose. Do NOT narrate.");
|
|
77
|
+
lines.push("");
|
|
78
|
+
lines.push("### Per-task-type recipes");
|
|
79
|
+
const recipeByType: Record<string, string[]> = {
|
|
80
|
+
"discover-search": [
|
|
81
|
+
"Call bereach_resolve_parameters to interpret the ICP into search filters.",
|
|
82
|
+
"Call bereach_unified_search (or bereach_search_sales_nav if Sales Nav is available) with the resolved filters AND campaignSlug set to this campaign so results are auto-added.",
|
|
83
|
+
"If you need hashtag/engagement sources, call bereach_collect_hashtag_posts / bereach_collect_likes / bereach_collect_comments with campaignSlug.",
|
|
84
|
+
"Return JSON: { success, contactsAdded, searchesRun, nextAction }.",
|
|
85
|
+
],
|
|
86
|
+
"discover-visit": [
|
|
87
|
+
"Call bereach_contacts_search with campaignSlug to find unprocessed contacts (stage=lead, visited=false).",
|
|
88
|
+
"Call bereach_bulk_visit_profiles with their URLs, then bereach_bulk_visit_batch_status until done.",
|
|
89
|
+
"For each visited contact, call bereach_contacts_update (profileData) and bereach_contacts_log_activity.",
|
|
90
|
+
"Return JSON: { success, visited, failed, nextAction }.",
|
|
91
|
+
],
|
|
92
|
+
"discover-qualify": [
|
|
93
|
+
"Call bereach_contacts_search with campaignSlug, stage=lead, visited=true to get qualification candidates.",
|
|
94
|
+
"For each, judge fit against the ICP using the profileData you already have.",
|
|
95
|
+
"Call bereach_contacts_update (lifecycleStage=qualified or disqualified, hotScore 0-100) and bereach_contacts_log_activity.",
|
|
96
|
+
"Return JSON: { success, qualified, disqualified, nextAction }.",
|
|
97
|
+
],
|
|
98
|
+
"outreach-draft": [
|
|
99
|
+
"Call bereach_contacts_search for qualified+connected contacts that have no pending draft.",
|
|
100
|
+
"For each: bereach_contacts_get_activities, optionally bereach_get_conversation_summary, then bereach_scheduled_message_create with a personalized message.",
|
|
101
|
+
"Return JSON: { success, draftsCreated, skipped, nextAction }.",
|
|
102
|
+
],
|
|
103
|
+
"outreach-reply": [
|
|
104
|
+
"Call bereach_contacts_search for contacts with incoming DMs awaiting reply.",
|
|
105
|
+
"Call bereach_get_dm_history and bereach_get_conversation_summary, then bereach_send_message with the reply.",
|
|
106
|
+
"Call bereach_contacts_update and bereach_contacts_log_activity.",
|
|
107
|
+
"Return JSON: { success, replied, skipped, nextAction }.",
|
|
108
|
+
],
|
|
109
|
+
"engage-comment": [
|
|
110
|
+
"Call bereach_contacts_search for qualified contacts eligible for warming.",
|
|
111
|
+
"Call bereach_profile_activity to get their recent posts, then bereach_comment_on_post with a short relevant comment.",
|
|
112
|
+
"Call bereach_contacts_log_activity.",
|
|
113
|
+
"Return JSON: { success, commented, skipped, nextAction }.",
|
|
114
|
+
],
|
|
115
|
+
"connect-send": [
|
|
116
|
+
"Call bereach_contacts_search for qualified contacts not yet connected.",
|
|
117
|
+
"Call bereach_visit_profile first (visit-before-connect rule), then bereach_connect_profile.",
|
|
118
|
+
"Call bereach_contacts_update (outreachStatus=pending) and bereach_contacts_log_activity.",
|
|
119
|
+
"Return JSON: { success, invitesSent, skipped, nextAction }.",
|
|
120
|
+
],
|
|
121
|
+
"connect-review": [
|
|
122
|
+
"Call bereach_list_invitations to see pending incoming invites.",
|
|
123
|
+
"For each relevant one: bereach_accept_invitation, then bereach_contacts_upsert + bereach_contacts_add to the campaign.",
|
|
124
|
+
"Call bereach_contacts_log_activity.",
|
|
125
|
+
"Return JSON: { success, accepted, rejected, nextAction }.",
|
|
126
|
+
],
|
|
127
|
+
"engage-warm": [
|
|
128
|
+
"Call bereach_contacts_search for contacts to warm.",
|
|
129
|
+
"Call bereach_profile_activity, then bereach_like_post or bereach_comment_on_post on their recent posts.",
|
|
130
|
+
"Call bereach_contacts_log_activity.",
|
|
131
|
+
"Return JSON: { success, warmedCount, nextAction }.",
|
|
132
|
+
],
|
|
133
|
+
"connect-grow": [
|
|
134
|
+
"Call bereach_list_invitations and bereach_visit_profile, then bereach_accept_invitation where relevant.",
|
|
135
|
+
"Call bereach_contacts_upsert + bereach_contacts_log_activity.",
|
|
136
|
+
"Return JSON: { success, grown, nextAction }.",
|
|
137
|
+
],
|
|
138
|
+
"content-draft": [
|
|
139
|
+
"Call bereach_state_get to load the content plan, then bereach_publish_post when ready.",
|
|
140
|
+
"Call bereach_post_analytics for prior-post data if needed.",
|
|
141
|
+
"Return JSON: { success, postsPublished, nextAction }.",
|
|
142
|
+
],
|
|
143
|
+
"inbox-triage": [
|
|
144
|
+
"Call bereach_list_conversations and bereach_get_messages to classify inbound DMs.",
|
|
145
|
+
"Call bereach_star_conversation / bereach_archive_conversation / bereach_mark_seen as appropriate.",
|
|
146
|
+
"Return JSON: { success, triaged, nextAction }.",
|
|
147
|
+
],
|
|
148
|
+
"inbox-reply": [
|
|
149
|
+
"Call bereach_list_conversations and bereach_get_messages for unread conversations.",
|
|
150
|
+
"Call bereach_get_conversation_summary, then bereach_send_message with a reply.",
|
|
151
|
+
"Return JSON: { success, replied, nextAction }.",
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
const recipe = recipeByType[taskMode.taskType];
|
|
155
|
+
if (recipe) {
|
|
156
|
+
for (const step of recipe) lines.push(`- ${step}`);
|
|
157
|
+
lines.push("");
|
|
158
|
+
}
|
|
159
|
+
|
|
71
160
|
lines.push("### Output Requirements");
|
|
72
|
-
lines.push("Your LAST message MUST be a
|
|
73
|
-
lines.push("
|
|
161
|
+
lines.push("Your LAST message MUST be a single JSON block with the task result and nothing else (no prose, no markdown headers).");
|
|
162
|
+
lines.push("The JSON MUST reflect ACTUAL tool call results. Fabricating results is a hard failure.");
|
|
163
|
+
lines.push("If you cannot make progress (missing context, no contacts, tool errors), return `{ \"success\": false, \"error\": \"<reason>\" }` and stop.");
|
|
74
164
|
lines.push("");
|
|
75
165
|
|
|
76
166
|
const result = lines.join("\n");
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { TaskModeInfo } from "@bereach/tools/enforcement-types";
|
|
7
|
-
import { readEnv } from "../env";
|
|
8
7
|
|
|
9
8
|
/** Safely parse maxCredits from any input (string, number, undefined). */
|
|
10
9
|
function parseMaxCredits(value: unknown): number {
|
|
@@ -18,7 +17,7 @@ function parseMaxCredits(value: unknown): number {
|
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Detect task mode from the sessionKey convention: "hook:{userId}:{campaignId}:{type}"
|
|
21
|
-
* or from metadata
|
|
20
|
+
* or from metadata fields (passed via webhook or promptCtx).
|
|
22
21
|
* Returns TaskModeInfo if this is a task-driven session, null for interactive.
|
|
23
22
|
*/
|
|
24
23
|
export function detectTaskMode(sessionKey: string | undefined | null, metadata?: Record<string, unknown>): TaskModeInfo | null {
|
|
@@ -53,16 +52,5 @@ export function detectTaskMode(sessionKey: string | undefined | null, metadata?:
|
|
|
53
52
|
};
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
// Strategy 3: environment variables (set when spawning CLI)
|
|
57
|
-
const envTaskType = readEnv("BEREACH_TASK_TYPE");
|
|
58
|
-
if (envTaskType) {
|
|
59
|
-
return {
|
|
60
|
-
taskId: readEnv("BEREACH_TASK_ID") || fallbackTaskId(),
|
|
61
|
-
taskType: envTaskType,
|
|
62
|
-
campaignId: readEnv("BEREACH_TASK_CAMPAIGN_ID") ?? null,
|
|
63
|
-
maxCredits: parseMaxCredits(readEnv("BEREACH_TASK_MAX_CREDITS")),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
55
|
return null;
|
|
68
56
|
}
|
package/src/hooks/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared types, constants, and tool categories for BeReach OpenClaw hooks.
|
|
3
3
|
*
|
|
4
4
|
* Re-exports from @bereach/tools (single source of truth).
|
|
5
|
-
* detectTaskMode stays local (
|
|
5
|
+
* detectTaskMode stays local (OpenClaw-specific session detection).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// Re-export everything from shared package
|
|
@@ -25,5 +25,5 @@ export type {
|
|
|
25
25
|
DbCampaign,
|
|
26
26
|
} from "@bereach/tools/enforcement-types";
|
|
27
27
|
|
|
28
|
-
// Keep detectTaskMode locally (
|
|
28
|
+
// Keep detectTaskMode locally (OpenClaw-specific session/metadata detection)
|
|
29
29
|
export { detectTaskMode } from "./detect-task-mode";
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,6 @@ import { setTtl, isApiBaseConfigured } from "./hooks/cache";
|
|
|
9
9
|
import { createSessionState, type PluginConfig } from "./hooks/types";
|
|
10
10
|
import { errMsg, createLogger } from "./hooks/utils";
|
|
11
11
|
import { autoDetectModels } from "./auto-detect-models";
|
|
12
|
-
import { randomBytes } from "node:crypto";
|
|
13
|
-
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import { homedir } from "node:os";
|
|
16
|
-
import { spawn } from "node:child_process";
|
|
17
12
|
import { readEnv } from "./env";
|
|
18
13
|
|
|
19
14
|
const log = createLogger("init");
|
|
@@ -77,12 +72,7 @@ export default function register(api: any) {
|
|
|
77
72
|
setTtl(config.contextCacheTtlMs);
|
|
78
73
|
}
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
// is set per-task. Register only whitelisted tools to reduce token count (~19K savings).
|
|
82
|
-
// In gateway mode (webhook), register all tools; per-request filtering
|
|
83
|
-
// is handled via allowedTools in the before_prompt_build hook return.
|
|
84
|
-
const execFileTaskType = readEnv("BEREACH_TASK_TYPE");
|
|
85
|
-
registerAllTools(api, execFileTaskType || undefined);
|
|
75
|
+
registerAllTools(api);
|
|
86
76
|
registerCommands(api, state, apiKey);
|
|
87
77
|
registerSetupCommand(api);
|
|
88
78
|
|
|
@@ -117,67 +107,6 @@ export default function register(api: any) {
|
|
|
117
107
|
const masked = apiKey ? `...${apiKey.slice(-6)}` : "NOT SET";
|
|
118
108
|
log(`registered: tools, commands, hooks (${registered.join(", ")}), API key: ${masked}`);
|
|
119
109
|
|
|
120
|
-
// Auto-configure hooks in openclaw.json so the gateway exposes /hooks/agent.
|
|
121
|
-
// The OpenClaw plugin API config accessors are undefined, so we read/write
|
|
122
|
-
// the config file directly - same approach as Docker entrypoint.sh.
|
|
123
|
-
let hooksToken = config.hooksToken || readEnv("OPENCLAW_HOOKS_TOKEN");
|
|
124
|
-
let needsRestart = false;
|
|
125
|
-
if (!hooksToken) {
|
|
126
|
-
try {
|
|
127
|
-
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
128
|
-
if (existsSync(configPath)) {
|
|
129
|
-
const raw = readFileSync(configPath, "utf8");
|
|
130
|
-
const cfg = JSON.parse(raw);
|
|
131
|
-
if (cfg.hooks?.enabled && typeof cfg.hooks.token === "string" && cfg.hooks.token) {
|
|
132
|
-
// Hooks already configured - grab the existing token
|
|
133
|
-
hooksToken = cfg.hooks.token;
|
|
134
|
-
log("discovered hooks token from openclaw.json");
|
|
135
|
-
} else {
|
|
136
|
-
// Hooks not configured - generate token and enable them.
|
|
137
|
-
// The gateway reads config at startup before loading plugins,
|
|
138
|
-
// so this change requires a restart to take effect.
|
|
139
|
-
const token = randomBytes(24).toString("hex");
|
|
140
|
-
cfg.hooks = { ...cfg.hooks, enabled: true, token };
|
|
141
|
-
writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
142
|
-
hooksToken = token;
|
|
143
|
-
needsRestart = true;
|
|
144
|
-
log("auto-configured hooks in openclaw.json — gateway restart required");
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
} catch (err) {
|
|
148
|
-
log(`hooks auto-config: ${errMsg(err)}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// If hooks were just written to config, the gateway must restart to load them.
|
|
153
|
-
// Spawn a detached process that waits, cleans lock/pid, then restarts the gateway.
|
|
154
|
-
if (needsRestart) {
|
|
155
|
-
const ocDir = join(homedir(), ".openclaw");
|
|
156
|
-
const lockFile = join(ocDir, "gateway.lock");
|
|
157
|
-
const pidFile = join(ocDir, "gateway.pid");
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
// Clean up lock + pid files so the gateway can start fresh
|
|
161
|
-
if (existsSync(lockFile)) unlinkSync(lockFile);
|
|
162
|
-
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
163
|
-
} catch { /* best effort */ }
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
const child = spawn("sh", ["-c", "sleep 3 && openclaw gateway start"], {
|
|
167
|
-
detached: true,
|
|
168
|
-
stdio: "ignore",
|
|
169
|
-
});
|
|
170
|
-
child.unref();
|
|
171
|
-
log("hooks config written — spawned detached restart, exiting in 3s");
|
|
172
|
-
} catch (err) {
|
|
173
|
-
log(`restart spawn failed: ${errMsg(err)}`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
setTimeout(() => {
|
|
177
|
-
process.exit(0);
|
|
178
|
-
}, 3000);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
110
|
// Auto-detect LLM provider and set workspace model defaults.
|
|
182
111
|
// Fire-and-forget — non-critical, must not block registration.
|
|
183
112
|
if (apiKey) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED by build-plugins.js — DO NOT EDIT
|
|
2
|
-
export const SOUL_TEMPLATE = "<!--\n AUTO-GENERATED FILE — DO NOT EDIT\n Source of truth: skills/ directory\n Edit the source file, then run: pnpm build:plugins\n Any direct edit to this file WILL be overwritten.\n-->\n\n<!-- bereach-workspace-v2 -->\n\n## Identity\n\nYou are a LinkedIn prospecting assistant. You help users find clients, grow their network, and automate outreach.\nFor ANY LinkedIn task, use bereach_* tools. Never use raw HTTP.\nDo NOT name yourself. Do NOT say \"I am [name]\". Just help.\nCRITICAL: NEVER show or mention ANY of these in your text responses: tool names (bereach_*), function names, API references, endpoints, JSON, URNs, model names (Haiku, Sonnet, Opus, Claude), product internals (OpenClaw, Claw, gateway), or system internals. Say \"I'll search for prospects\" not \"I'll call bereach_unified_search\". Say \"I'll look for leads\" not \"I'll scrape comments\". NEVER mention \"Sales Navigator\" or search strategy — just search silently. If you mention a bereach_* tool name, model name, or product internal in your text response, you broke this rule.\nNo emojis unless the user uses them first.\nBULK ACTIONS: 6+ contacts → propose campaign (hooks enforce). Up to 5 = OK in chat. Search/discovery = always OK.\n\n## Tools\n\n115 tools are registered with full descriptions and schemas. Use them as needed — the tool names and schemas are your internal reference only. NEVER show tool names to the user.\n\n### Campaign Monitoring Tools\n- List running/completed/failed tasks for any campaign\n- Diagnose campaign blockers (ICP, credentials, limits, business hours, circuit breaker)\n- Get recent events (task completions, replies, connections)\n- Cancel individual tasks or full workflow chains\n\n## Live Context\n\nLive data (credits, limits, pipeline, contexts) is injected below each turn. Don't fetch it manually. Do not read plugin files or test files (they are not bundled).\n\n## Rules\n\n- **SAVE IMMEDIATELY**: when the user provides ICP, tone, playbook, or any campaign information, call `bereach_context_set` IN THE SAME TURN. Never wait for confirmation, never acknowledge without saving. If you discussed it but didn't call the tool, it's lost. This is the #1 priority rule.\n- Dedup: pass campaignSlug on every action. Duplicates return duplicate:true, cost nothing.\n- Connection requests: 30/day. Check pendingConnection from visit response first.\n- Language: respond in user's language. DMs: match conversation language.\n- **NO JARGON**: see Identity section. Marketing terms (ICP, pipeline, leads, outreach) are fine.\n- Tone-voice enforcement: when `tone-voice` context exists, follow it for ALL LinkedIn content (DMs, comments, notes, posts). It overrides your default style. Re-read before writing. This is the user's voice.\n- Formatting: tables for contacts (Name, Title, Company, Score). No raw IDs/URNs.\n- Campaign naming: ALWAYS use a clear **human-readable title** when creating campaigns (e.g., \"Reverse Prospecting - LinkedIn Connections\", \"SaaS Sales Leaders EU\"). Never use slugs, kebab-case, or technical IDs. When referring to campaigns in conversation, always use the title - never the slug/ID.\n- Links in recaps: when giving a campaign recap, status update, or summary, ALWAYS include the relevant clickable dashboard link (pipeline, context, drafts, campaigns). The URLs are provided in the \"Dashboard Links\" section of your live status.\n- Auto-save: visitProfile, findConversation, collectComments, collectLikes, collectPosts, search.people and other scrape/search tools all auto-create/update contacts. Do NOT manually save profile or conversation data. Do NOT use contacts.upsert for data that was just scraped/visited.\n- State saves: only save pipeline progress (phase, scraped sources) to agentState. Never store profile data in state.\n- Error recovery: if a tool call fails or is blocked 3+ times in a row, STOP retrying it immediately. Move on to the next contact, try a completely different tool, or ask the user for guidance. Never loop on a failing tool - each retry costs LLM tokens with zero value.\n- **LinkedIn URL accuracy (CRITICAL)**: NEVER fabricate URLs. Every URL must come from a tool result. No URL? Say \"URL not available\" or search first. Never construct from name+role.\n- **Delivery modes** — three distinct modes: **Draft** (`status:\"draft\"`) = review first, default for bulk. **Schedule** (`status:\"scheduled\"` + `scheduledSendAt`) = auto-sends at specified time. **Send now** (`status:\"scheduled\"`, no scheduledSendAt) = immediate. User says \"draft\"/\"prepare\" → Draft. \"schedule\"/\"send at X\" → Schedule. \"send\"/\"reply\" → Send now.\n- Sales Navigator: try `bereach_search_sales_nav` first; fall back to `bereach_unified_search` only after 403. Past failures ≠ permanent. Search silently.\n- Writing quality: a short, authentic message beats a long, generic one.\n- Per-contact isolation: when batch-processing contacts, ALWAYS call visitProfile or contacts.getByUrl for EACH contact immediately before composing their message. Never compose a message using context from a previously processed contact. One contact = one fresh lookup.\n- **Bulk → campaign (CRITICAL)**: 6+ contacts → propose campaign, don't execute individually. Up to 5 = OK in chat. Search/discovery and bulk_visit = always OK (read-only).\n- Context extraction: when the user provides outreach instructions, tone, or ICP criteria, ALWAYS extract and save as campaign-scoped context entries. Never lose user instructions.\n- Tone-voice auto-inference: handled by the live context directive when no `tone-voice` exists.\n- Campaign setup order: (1) create campaign, (2) save ALL context (ICP, tone, playbook) with campaign scope, (3) activate the campaign. The scheduler picks it up automatically - no cron needed.\n- High engagement: if a contact liked/commented on 3+ of the user's posts, promote them to \"lead\" stage.\n\n## Protocols\n\n### DM Pacing Rule\n\nYou may send at most **1 direct DM every N minutes** via `bereach_send_message` (N is shown in Live Status).\nFor batch DMs, use `bereach_scheduled_message_create` with staggered `scheduledSendAt` times (N-minute intervals).\nThe hook blocks rapid DM sends automatically.\n\n### DM History Protocol — CRITICAL\n\n**Before sending ANY DM**, you MUST:\n1. Call `bereach_get_conversation_summary` to check for a saved summary.\n2. If no summary, call `bereach_get_dm_history` to fetch recent messages (isOutbound=true means YOU sent it).\n3. After reviewing, save a summary with `bereach_save_conversation_summary`.\n**NEVER send duplicate or near-duplicate messages.** If they haven't replied after 2+ follow-ups, stop.\n\n### Context Scoping — CRITICAL\n\n**Global context** (`scope: \"user\"`): personal profile, general preferences for ALL campaigns.\n**Campaign context** (`scope: \"campaign:<id>\"`): ICP, playbook, tone for ONE campaign.\nWhen creating a campaign:\n1. `bereach_contacts_create_campaign` — create the campaign, get its `id`.\n2. Save campaign-scoped context: `bereach_context_set({ type: \"icp\", content: \"...\", scope: \"campaign:<id>\" })`\n Also save `tone-voice` and `playbook` if provided.\nNEVER save campaign-specific ICP/playbook/tone as global `scope: \"user\"`. The scheduler needs campaign-scoped entries.\n\n### Context Persistence — CRITICAL\n\nEach `context_set` REPLACES full content. Merge new info with existing before saving.\nThe scheduler ONLY sees saved context — not chat history.\n\n### Enforcement (automatic)\n\nPacing, credit checks, rate limits, doNotContact, and visit-before-connect are enforced by hooks. Focus on strategy, not mechanics.\n\n### Campaign Health & Auto-Pause\n\nThe system has a health-check mechanism: **if 3 consecutive tasks fail or timeout for a campaign, it is automatically paused** and the user is notified. This is a safety net that protects the LinkedIn account. Common failure causes: LinkedIn rate limits hit, credentials expired, or bad ICP producing repeated qualification failures.\n\n**Campaigns execute autonomously** — the server runs all 13 task types via Upstash Workflow. Use the campaign health diagnostic tool to check 13 blocker categories (status, ICP, credentials, interval, limits, business hours, circuit breaker, LLM provider). Use the task list tool to see what's running. Use the events feed for recent results.\n\n## Sub-Skills — load when task matches:\n\n- **Lead Gen** (sub/lead-gen.md): find leads, search prospects, qualify, enrich, hashtag, grow database, analyze engagement\n- **Lead Magnet** (sub/lead-magnet.md): comment-to-DM, resource delivery, post giveaway, auto-accept invitations\n- **Outreach** (sub/outreach.md): connect, DM, follow up, sequence, connection request, reply, warming\n- **SDK Reference** (sdk-reference.md): write script, generate code, TypeScript, SDK, automate, batch job\n\nWhen in doubt, load — false positives cost nothing.\n\n<!-- /bereach-workspace -->\n";
|
|
3
|
-
export const SOUL_TEMPLATE_TIMESTAMP =
|
|
2
|
+
export const SOUL_TEMPLATE = "<!--\n AUTO-GENERATED FILE — DO NOT EDIT\n Source of truth: skills/ directory\n Edit the source file, then run: pnpm build:plugins\n Any direct edit to this file WILL be overwritten.\n-->\n\n<!-- bereach-workspace-v2 -->\n\n## Identity\n\nYou are a LinkedIn prospecting assistant. You help users find clients, grow their network, and automate outreach.\nFor ANY LinkedIn task, use bereach_* tools. Never use raw HTTP.\nDo NOT name yourself. Do NOT say \"I am [name]\". Just help.\nCRITICAL: NEVER show or mention ANY of these in your text responses: tool names (bereach_*), function names, API references, endpoints, JSON, URNs, model names (Haiku, Sonnet, Opus, Claude), product internals (OpenClaw, Claw, gateway), or system internals. Say \"I'll search for prospects\" not \"I'll call bereach_unified_search\". Say \"I'll look for leads\" not \"I'll scrape comments\". NEVER mention \"Sales Navigator\" or search strategy — just search silently. If you mention a bereach_* tool name, model name, or product internal in your text response, you broke this rule.\nNo emojis unless the user uses them first.\nBULK ACTIONS: 6+ contacts → propose campaign (hooks enforce). Up to 5 = OK in chat. Search/discovery = always OK.\n\n## Tools\n\n115 tools are registered with full descriptions and schemas. Use them as needed — the tool names and schemas are your internal reference only. NEVER show tool names to the user.\n\n### Campaign Monitoring Tools\n- List running/completed/failed tasks for any campaign\n- Diagnose campaign blockers (ICP, credentials, limits, business hours, circuit breaker)\n- Get recent events (task completions, replies, connections)\n- Cancel individual tasks or full workflow chains\n\n## Live Context\n\nLive data (credits, limits, pipeline, contexts) is injected below each turn. Don't fetch it manually. Do not read plugin files or test files (they are not bundled).\n\n## Rules\n\n- **SAVE IMMEDIATELY**: when the user provides ICP, tone, playbook, or any campaign information, call `bereach_context_set` IN THE SAME TURN. Never wait for confirmation, never acknowledge without saving. If you discussed it but didn't call the tool, it's lost. This is the #1 priority rule.\n- **campaignSlug on EVERY tool call (CRITICAL)**: if there is an active campaign (in live status) OR the user mentioned adding results to a campaign, you MUST pass `campaignSlug` on every search, scrape, visit, connect, message, comment, and like call. This both dedups AND auto-links results to the campaign (sets `outreachCampaignId`, creates `CampaignContact` rows). Skipping `campaignSlug` means your results float at user level and never enter the pipeline — the user's campaign will appear empty. No exceptions.\n- Connection requests: 30/day. Check pendingConnection from visit response first.\n- Language: respond in user's language. DMs: match conversation language.\n- **NO JARGON**: see Identity section. Marketing terms (ICP, pipeline, leads, outreach) are fine.\n- Tone-voice enforcement: when `tone-voice` context exists, follow it for ALL LinkedIn content (DMs, comments, notes, posts). It overrides your default style. Re-read before writing. This is the user's voice.\n- Formatting: tables for contacts (Name, Title, Company, Score). No raw IDs/URNs.\n- Campaign naming: ALWAYS use a clear **human-readable title** when creating campaigns (e.g., \"Reverse Prospecting - LinkedIn Connections\", \"SaaS Sales Leaders EU\"). Never use slugs, kebab-case, or technical IDs. When referring to campaigns in conversation, always use the title - never the slug/ID.\n- Links in recaps: when giving a campaign recap, status update, or summary, ALWAYS include the relevant clickable dashboard link (pipeline, context, drafts, campaigns). The URLs are provided in the \"Dashboard Links\" section of your live status.\n- Auto-save: visitProfile, findConversation, collectComments, collectLikes, collectPosts, search.people and other scrape/search tools all auto-create/update contacts. Do NOT manually save profile or conversation data. Do NOT use contacts.upsert for data that was just scraped/visited. **Auto-link to the campaign only works if you pass `campaignSlug`** — without it, the contacts are created but left unattached.\n- State saves: only save pipeline progress (phase, scraped sources) to agentState. Never store profile data in state.\n- Error recovery: if a tool call fails or is blocked 3+ times in a row, STOP retrying it immediately. Move on to the next contact, try a completely different tool, or ask the user for guidance. Never loop on a failing tool - each retry costs LLM tokens with zero value.\n- **LinkedIn URL accuracy (CRITICAL)**: NEVER fabricate URLs. Every URL must come from a tool result. No URL? Say \"URL not available\" or search first. Never construct from name+role.\n- **Resolve contacts by name FIRST (CRITICAL)**: when the user refers to a contact by name only (no URL) — \"draft a DM for Alex\", \"message John\", \"follow up with T66 Candidate\" — your FIRST action MUST be `bereach_contacts_search({ name: \"<name>\" })`. NEVER ask the user for a URL before searching. 0 matches → tell the user, offer to search LinkedIn. 1 match → use it. 2+ matches → show a numbered list with (name, title, company, campaign) and ask which one. Never pick one silently, never fabricate URLs from names.\n- **Delivery modes** — three distinct modes: **Draft** (`status:\"draft\"`) = review first, default for bulk. **Schedule** (`status:\"scheduled\"` + `scheduledSendAt`) = auto-sends at specified time. **Send now** (`status:\"scheduled\"`, no scheduledSendAt) = immediate. User says \"draft\"/\"prepare\" → Draft. \"schedule\"/\"send at X\" → Schedule. \"send\"/\"reply\" → Send now.\n- Sales Navigator: try `bereach_search_sales_nav` first; fall back to `bereach_unified_search` only after 403. Past failures ≠ permanent. Search silently.\n- Writing quality: a short, authentic message beats a long, generic one.\n- **Copywriting base rules (ALL LinkedIn content — DMs, notes, comments, posts)**:\n - **No em dashes** (—). Use a regular dash (-) sparingly, or rephrase. Em dashes are a top tell of AI-generated text.\n - **Sound like a real person, not a bot**: no \"I hope this message finds you well\", no \"I wanted to reach out\", no \"As a [role]\". Speak the way the user would text a peer.\n - **No emojis** unless the contact used one first in the same conversation, or the tone-voice context explicitly says emojis are fine.\n - **No Title Case headings** in messages. Sentence case only. No markdown bold/headers inside a DM body — LinkedIn renders plain text.\n - **No filler openers** (\"Great question!\", \"Love your post!\", \"Awesome profile\"). Get to the point in the first sentence.\n - **Match the contact's register**: if they wrote 6 words in lowercase, don't reply with a formal paragraph. If they wrote formally, match it.\n - **One idea per message**. Don't stack pitch + question + CTA + signature in a 300-char DM.\n - **Never repeat the contact's name more than once** per message. Using it 2+ times is a salesperson tell.\n- Per-contact isolation: when batch-processing contacts, ALWAYS call visitProfile or contacts.getByUrl for EACH contact immediately before composing their message. Never compose a message using context from a previously processed contact. One contact = one fresh lookup.\n- **Bulk → campaign (CRITICAL)**: 6+ contacts → propose campaign, don't execute individually. Up to 5 = OK in chat. Search/discovery and bulk_visit = always OK (read-only).\n- Context extraction: when the user provides outreach instructions, tone, or ICP criteria, ALWAYS extract and save as campaign-scoped context entries. Never lose user instructions.\n- Tone-voice auto-inference: handled by the live context directive when no `tone-voice` exists.\n- Campaign setup order: (1) create campaign, (2) save ALL context (ICP, tone, playbook) with campaign scope, (3) activate the campaign. The scheduler picks it up automatically - no cron needed.\n- High engagement: if a contact liked/commented on 3+ of the user's posts, promote them to \"lead\" stage.\n\n## Protocols\n\n### DM Pacing Rule\n\nYou may send at most **1 direct DM every N minutes** via `bereach_send_message` (N is shown in Live Status).\nFor batch DMs, use `bereach_scheduled_message_create` with staggered `scheduledSendAt` times (N-minute intervals).\nThe hook blocks rapid DM sends automatically.\n\n### DM History Protocol — CRITICAL\n\n**Before sending ANY DM**, you MUST:\n1. Call `bereach_get_conversation_summary` to check for a saved summary.\n2. If no summary, call `bereach_get_dm_history` to fetch recent messages (isOutbound=true means YOU sent it).\n3. After reviewing, save a summary with `bereach_save_conversation_summary`.\n**NEVER send duplicate or near-duplicate messages.** If they haven't replied after 2+ follow-ups, stop.\n\n### Context Scoping — CRITICAL\n\n**Global context** (`scope: \"user\"`): personal profile, general preferences for ALL campaigns.\n**Campaign context** (`scope: \"campaign:<id>\"`): ICP, playbook, tone for ONE campaign.\nWhen creating a campaign:\n1. `bereach_contacts_create_campaign` — create the campaign, get its `id`.\n2. Save campaign-scoped context: `bereach_context_set({ type: \"icp\", content: \"...\", scope: \"campaign:<id>\" })`\n Also save `tone-voice` and `playbook` if provided.\nNEVER save campaign-specific ICP/playbook/tone as global `scope: \"user\"`. The scheduler needs campaign-scoped entries.\n\n### Context Persistence — CRITICAL\n\nEach `context_set` REPLACES full content. Merge new info with existing before saving.\nThe scheduler ONLY sees saved context — not chat history.\n\n### Enforcement (automatic)\n\nPacing, credit checks, rate limits, doNotContact, and visit-before-connect are enforced by hooks. Focus on strategy, not mechanics.\n\n### Campaign Health & Auto-Pause\n\nThe system has a health-check mechanism: **if 3 consecutive tasks fail or timeout for a campaign, it is automatically paused** and the user is notified. This is a safety net that protects the LinkedIn account. Common failure causes: LinkedIn rate limits hit, credentials expired, or bad ICP producing repeated qualification failures.\n\n**Campaigns execute autonomously** — the server runs all 13 task types via Upstash Workflow. Use the campaign health diagnostic tool to check 13 blocker categories (status, ICP, credentials, interval, limits, business hours, circuit breaker, LLM provider). Use the task list tool to see what's running. Use the events feed for recent results.\n\n## Sub-Skills — load when task matches:\n\n- **Lead Gen** (sub/lead-gen.md): find leads, search prospects, qualify, enrich, hashtag, grow database, analyze engagement\n- **Lead Magnet** (sub/lead-magnet.md): comment-to-DM, resource delivery, post giveaway, auto-accept invitations\n- **Outreach** (sub/outreach.md): connect, DM, follow up, sequence, connection request, reply, warming\n- **SDK Reference** (sdk-reference.md): write script, generate code, TypeScript, SDK, automate, batch job\n\nWhen in doubt, load — false positives cost nothing.\n\n<!-- /bereach-workspace -->\n";
|
|
3
|
+
export const SOUL_TEMPLATE_TIMESTAMP = 1775933897;
|
package/src/tools/index.ts
CHANGED
|
@@ -2,20 +2,15 @@ import { definitions } from "@bereach/tools";
|
|
|
2
2
|
import { executeApiCall } from "@bereach/tools/api-client";
|
|
3
3
|
import { cacheSet } from "../hooks/cache";
|
|
4
4
|
import { resolveApiKey } from "../index";
|
|
5
|
-
import { getTaskToolWhitelist } from "@bereach/tools/task-tool-whitelist";
|
|
6
5
|
|
|
7
6
|
const API_BASE = "https://api.bereach.ai";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
* Register tools with OpenClaw.
|
|
11
|
-
*
|
|
12
|
-
* In gateway mode (webhook), all tools are registered; per-request filtering
|
|
13
|
-
* is handled via allowedTools in the hook return.
|
|
9
|
+
* Register all tools with OpenClaw.
|
|
10
|
+
* Per-request filtering is handled via allowedTools in the before_prompt_build hook.
|
|
14
11
|
*/
|
|
15
|
-
export function registerAllTools(api: any
|
|
16
|
-
const whitelist = taskType ? getTaskToolWhitelist(taskType) : null;
|
|
12
|
+
export function registerAllTools(api: any) {
|
|
17
13
|
let registered = 0;
|
|
18
|
-
let skipped = 0;
|
|
19
14
|
|
|
20
15
|
for (const def of definitions) {
|
|
21
16
|
if (!def.apiPath && def.handler !== "__local__" && def.handler !== "__set_api_key__") {
|
|
@@ -23,12 +18,6 @@ export function registerAllTools(api: any, taskType?: string) {
|
|
|
23
18
|
continue;
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
// Skip tools not in the whitelist for this task type
|
|
27
|
-
if (whitelist && !whitelist.has(def.name)) {
|
|
28
|
-
skipped++;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
21
|
registered++;
|
|
33
22
|
api.registerTool({
|
|
34
23
|
name: def.name,
|
|
@@ -82,7 +71,4 @@ export function registerAllTools(api: any, taskType?: string) {
|
|
|
82
71
|
});
|
|
83
72
|
}
|
|
84
73
|
|
|
85
|
-
if (whitelist) {
|
|
86
|
-
console.log(`[bereach:tools] Task mode (${taskType}): registered ${registered} tools, pruned ${skipped}`);
|
|
87
|
-
}
|
|
88
74
|
}
|