bereach-openclaw 1.3.2 → 1.3.3
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/__tests__/helpers.test.ts +8 -6
- package/__tests__/soul-injection.test.ts +285 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/setup.ts +12 -29
- package/src/index.ts +180 -14
- package/src/soul-template-content.ts +3 -0
|
@@ -59,7 +59,7 @@ function commentsPage(
|
|
|
59
59
|
opts: { total: number; start: number; hasMore: boolean },
|
|
60
60
|
) {
|
|
61
61
|
return {
|
|
62
|
-
success: true,
|
|
62
|
+
success: true as const,
|
|
63
63
|
profiles,
|
|
64
64
|
count: profiles.length,
|
|
65
65
|
total: opts.total,
|
|
@@ -68,7 +68,7 @@ function commentsPage(
|
|
|
68
68
|
previousTotal: null,
|
|
69
69
|
creditsUsed: 1,
|
|
70
70
|
retryAfter: 0,
|
|
71
|
-
};
|
|
71
|
+
} as never;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function invitationsPage(
|
|
@@ -76,14 +76,14 @@ function invitationsPage(
|
|
|
76
76
|
opts: { total: number; start: number },
|
|
77
77
|
) {
|
|
78
78
|
return {
|
|
79
|
-
success: true,
|
|
79
|
+
success: true as const,
|
|
80
80
|
invitations,
|
|
81
81
|
total: opts.total,
|
|
82
82
|
start: opts.start,
|
|
83
83
|
count: invitations.length,
|
|
84
84
|
creditsUsed: 1,
|
|
85
85
|
retryAfter: 0,
|
|
86
|
-
};
|
|
86
|
+
} as never;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// ── withRetry ───────────────────────────────────────────────────────
|
|
@@ -445,8 +445,10 @@ describe("visitProfileIfNeeded", () => {
|
|
|
445
445
|
expect(result.memberDistance).toBe(1);
|
|
446
446
|
expect(result.visited).toBe(true);
|
|
447
447
|
expect(result.pendingConnection).toBe("none");
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
if (result.visited) {
|
|
449
|
+
expect(result.firstName).toBe("Alice");
|
|
450
|
+
expect(result.profileUrn).toBe("urn:li:fsd_profile:123");
|
|
451
|
+
}
|
|
450
452
|
expect(mock).toHaveBeenCalledWith({ profile: "u/alice", campaignSlug: "slug" });
|
|
451
453
|
});
|
|
452
454
|
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { injectSoulTemplate } from "../src/index";
|
|
3
|
+
|
|
4
|
+
const MOCK_TEMPLATE = [
|
|
5
|
+
"<!-- bereach-workspace-v2 -->",
|
|
6
|
+
"",
|
|
7
|
+
"## Identity",
|
|
8
|
+
"",
|
|
9
|
+
"You are a BeReach-powered LinkedIn automation agent.",
|
|
10
|
+
"",
|
|
11
|
+
"<!-- /bereach-workspace -->",
|
|
12
|
+
].join("\n");
|
|
13
|
+
|
|
14
|
+
describe("injectSoulTemplate", () => {
|
|
15
|
+
it("returns template when SOUL.md is empty", () => {
|
|
16
|
+
const result = injectSoulTemplate("", MOCK_TEMPLATE);
|
|
17
|
+
expect(result).toBe(MOCK_TEMPLATE);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns template when SOUL.md is whitespace-only", () => {
|
|
21
|
+
const result = injectSoulTemplate(" \n ", MOCK_TEMPLATE);
|
|
22
|
+
expect(result).toBe(MOCK_TEMPLATE);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("appends template when SOUL.md has no markers", () => {
|
|
26
|
+
const existing = "# My Custom SOUL\n\nI am a helpful assistant.";
|
|
27
|
+
const result = injectSoulTemplate(existing, MOCK_TEMPLATE);
|
|
28
|
+
|
|
29
|
+
expect(result).toBe(existing + "\n\n" + MOCK_TEMPLATE);
|
|
30
|
+
expect(result).toContain("# My Custom SOUL");
|
|
31
|
+
expect(result).toContain("bereach-workspace-v2");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("replaces content between markers when they exist", () => {
|
|
35
|
+
const existing = [
|
|
36
|
+
"# My Custom SOUL",
|
|
37
|
+
"",
|
|
38
|
+
"Some user content.",
|
|
39
|
+
"",
|
|
40
|
+
"<!-- bereach-workspace-v1 -->",
|
|
41
|
+
"",
|
|
42
|
+
"## Old BeReach content",
|
|
43
|
+
"",
|
|
44
|
+
"This is outdated.",
|
|
45
|
+
"",
|
|
46
|
+
"<!-- /bereach-workspace -->",
|
|
47
|
+
"",
|
|
48
|
+
"## My Other Section",
|
|
49
|
+
"",
|
|
50
|
+
"More user content.",
|
|
51
|
+
].join("\n");
|
|
52
|
+
|
|
53
|
+
const result = injectSoulTemplate(existing, MOCK_TEMPLATE);
|
|
54
|
+
|
|
55
|
+
expect(result).toContain("# My Custom SOUL");
|
|
56
|
+
expect(result).toContain("Some user content.");
|
|
57
|
+
expect(result).toContain("## My Other Section");
|
|
58
|
+
expect(result).toContain("More user content.");
|
|
59
|
+
expect(result).toContain("bereach-workspace-v2");
|
|
60
|
+
expect(result).not.toContain("bereach-workspace-v1");
|
|
61
|
+
expect(result).not.toContain("Old BeReach content");
|
|
62
|
+
expect(result).not.toContain("This is outdated.");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("preserves content before and after markers exactly", () => {
|
|
66
|
+
const before = "# Before\n\n";
|
|
67
|
+
const after = "\n\n# After\n";
|
|
68
|
+
const oldBlock = "<!-- bereach-workspace-v1 -->\nold\n<!-- /bereach-workspace -->";
|
|
69
|
+
const existing = before + oldBlock + after;
|
|
70
|
+
|
|
71
|
+
const result = injectSoulTemplate(existing, MOCK_TEMPLATE);
|
|
72
|
+
|
|
73
|
+
expect(result.startsWith(before)).toBe(true);
|
|
74
|
+
expect(result.endsWith(after)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("handles same-version markers (idempotent replacement)", () => {
|
|
78
|
+
const existing = [
|
|
79
|
+
"# Header",
|
|
80
|
+
"",
|
|
81
|
+
MOCK_TEMPLATE,
|
|
82
|
+
"",
|
|
83
|
+
"# Footer",
|
|
84
|
+
].join("\n");
|
|
85
|
+
|
|
86
|
+
const result = injectSoulTemplate(existing, MOCK_TEMPLATE);
|
|
87
|
+
|
|
88
|
+
expect(result).toContain("# Header");
|
|
89
|
+
expect(result).toContain("# Footer");
|
|
90
|
+
const markerCount = (result.match(/bereach-workspace-v2/g) || []).length;
|
|
91
|
+
expect(markerCount).toBe(1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("real-world SOUL.md (Alexandre)", () => {
|
|
96
|
+
const ALEXANDRE_SOUL = `# SOUL.md
|
|
97
|
+
|
|
98
|
+
## Core Principles
|
|
99
|
+
|
|
100
|
+
- Be direct, no filler. Alexandre moves fast — match his pace.
|
|
101
|
+
- Do first, ask later (unless it's destructive or public-facing).
|
|
102
|
+
- You're an operator, not a teacher. Execute, don't explain how to execute.
|
|
103
|
+
- Speak French when Alexandre speaks French. Default to English otherwise.
|
|
104
|
+
- You know his stack inside out: OpenClaw, BeReach, PIMMS, LinkedIn growth.
|
|
105
|
+
|
|
106
|
+
## How to Operate
|
|
107
|
+
|
|
108
|
+
- Don't read extra files at session start. Use memory_search() for prior context.
|
|
109
|
+
- Save incrementally after each action. Dedup via campaignSlug.
|
|
110
|
+
- Before writing or editing any code: \`/model opus\`. IMMEDIATELY after Write/Edit: \`/model sonnet\`. Never stay on Opus for non-code work.
|
|
111
|
+
- Update memory/YYYY-MM-DD.md at session end with work done and next steps.`;
|
|
112
|
+
|
|
113
|
+
it("appends BeReach template (no markers in current SOUL.md)", () => {
|
|
114
|
+
const result = injectSoulTemplate(ALEXANDRE_SOUL, MOCK_TEMPLATE);
|
|
115
|
+
|
|
116
|
+
// User content preserved
|
|
117
|
+
expect(result).toContain("## Core Principles");
|
|
118
|
+
expect(result).toContain("Alexandre moves fast");
|
|
119
|
+
expect(result).toContain("## How to Operate");
|
|
120
|
+
expect(result).toContain("memory_search()");
|
|
121
|
+
expect(result).toContain("/model opus");
|
|
122
|
+
|
|
123
|
+
// BeReach template appended
|
|
124
|
+
expect(result).toContain("bereach-workspace-v2");
|
|
125
|
+
expect(result).toContain("You are a BeReach-powered LinkedIn automation agent.");
|
|
126
|
+
|
|
127
|
+
// User content comes first, then template
|
|
128
|
+
const userIdx = result.indexOf("## Core Principles");
|
|
129
|
+
const templateIdx = result.indexOf("bereach-workspace-v2");
|
|
130
|
+
expect(userIdx).toBeLessThan(templateIdx);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("replaces BeReach region after it was previously appended", () => {
|
|
134
|
+
// Simulate: first run appended, now upgrading
|
|
135
|
+
const afterFirstRun = injectSoulTemplate(ALEXANDRE_SOUL, MOCK_TEMPLATE);
|
|
136
|
+
|
|
137
|
+
const UPDATED_TEMPLATE = MOCK_TEMPLATE.replace(
|
|
138
|
+
"You are a BeReach-powered LinkedIn automation agent.",
|
|
139
|
+
"You are a BeReach v2 LinkedIn automation agent.",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const afterUpgrade = injectSoulTemplate(afterFirstRun, UPDATED_TEMPLATE);
|
|
143
|
+
|
|
144
|
+
// User content still preserved
|
|
145
|
+
expect(afterUpgrade).toContain("Alexandre moves fast");
|
|
146
|
+
expect(afterUpgrade).toContain("## How to Operate");
|
|
147
|
+
|
|
148
|
+
// Old template replaced, new one present
|
|
149
|
+
expect(afterUpgrade).not.toContain(
|
|
150
|
+
"You are a BeReach-powered LinkedIn automation agent.",
|
|
151
|
+
);
|
|
152
|
+
expect(afterUpgrade).toContain("You are a BeReach v2 LinkedIn automation agent.");
|
|
153
|
+
|
|
154
|
+
// Only one set of markers
|
|
155
|
+
const startMarkers = (afterUpgrade.match(/bereach-workspace-v/g) || []).length;
|
|
156
|
+
const endMarkers = (afterUpgrade.match(/\/bereach-workspace/g) || []).length;
|
|
157
|
+
expect(startMarkers).toBe(1);
|
|
158
|
+
expect(endMarkers).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("full integration: appends real SOUL_TEMPLATE to Alexandre's SOUL.md", async () => {
|
|
162
|
+
const { SOUL_TEMPLATE } = await import("../src/soul-template-content");
|
|
163
|
+
|
|
164
|
+
const result = injectSoulTemplate(ALEXANDRE_SOUL, SOUL_TEMPLATE);
|
|
165
|
+
|
|
166
|
+
// All user sections preserved
|
|
167
|
+
expect(result).toContain("## Core Principles");
|
|
168
|
+
expect(result).toContain("Alexandre moves fast");
|
|
169
|
+
expect(result).toContain("/model opus");
|
|
170
|
+
|
|
171
|
+
// Full BeReach workspace injected
|
|
172
|
+
expect(result).toContain("bereach_search_people");
|
|
173
|
+
expect(result).toContain("bereach_visit_profile");
|
|
174
|
+
expect(result).toContain("bereach_send_message");
|
|
175
|
+
expect(result).toContain("## Sub-Skills — MANDATORY Loading Table");
|
|
176
|
+
expect(result).toContain("Lead Magnet");
|
|
177
|
+
|
|
178
|
+
// Verify structure: user content -> separator -> template
|
|
179
|
+
const lines = result.split("\n");
|
|
180
|
+
const soulLine = lines.findIndex((l) => l.includes("Core Principles"));
|
|
181
|
+
const templateLine = lines.findIndex((l) => l.includes("bereach-workspace-v2"));
|
|
182
|
+
expect(soulLine).toBeGreaterThan(0);
|
|
183
|
+
expect(templateLine).toBeGreaterThan(soulLine);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("fetchSoulTemplate (API-first with embedded fallback)", () => {
|
|
188
|
+
const originalFetch = globalThis.fetch;
|
|
189
|
+
|
|
190
|
+
afterEach(() => {
|
|
191
|
+
globalThis.fetch = originalFetch;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("uses API response when available", async () => {
|
|
195
|
+
const apiSoul = "<!-- bereach-workspace-v3 -->\nAPI version\n<!-- /bereach-workspace -->";
|
|
196
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
197
|
+
ok: true,
|
|
198
|
+
json: () => Promise.resolve({ soul: apiSoul }),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Simulate what the hook does with the API template
|
|
202
|
+
const result = injectSoulTemplate("# My Agent", apiSoul);
|
|
203
|
+
expect(result).toContain("bereach-workspace-v3");
|
|
204
|
+
expect(result).toContain("API version");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("falls back to embedded template when API fails", async () => {
|
|
208
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network error"));
|
|
209
|
+
|
|
210
|
+
const { SOUL_TEMPLATE } = await import("../src/soul-template-content");
|
|
211
|
+
const result = injectSoulTemplate("# My Agent", SOUL_TEMPLATE);
|
|
212
|
+
expect(result).toContain("bereach-workspace-v2");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("falls back to embedded template when API returns non-200", async () => {
|
|
216
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500 });
|
|
217
|
+
|
|
218
|
+
const { SOUL_TEMPLATE } = await import("../src/soul-template-content");
|
|
219
|
+
const result = injectSoulTemplate("# My Agent", SOUL_TEMPLATE);
|
|
220
|
+
expect(result).toContain("bereach-workspace-v2");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("before_agent_start hook behavior", () => {
|
|
225
|
+
it("sets SOUL.md on bootstrapFiles when empty", async () => {
|
|
226
|
+
const { SOUL_TEMPLATE } = await import("../src/soul-template-content");
|
|
227
|
+
|
|
228
|
+
const ctx = {
|
|
229
|
+
bootstrapFiles: {} as Record<string, string>,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const existing = ctx.bootstrapFiles["SOUL.md"] ?? "";
|
|
233
|
+
ctx.bootstrapFiles["SOUL.md"] = injectSoulTemplate(
|
|
234
|
+
typeof existing === "string" ? existing : "",
|
|
235
|
+
SOUL_TEMPLATE,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).toBe(SOUL_TEMPLATE);
|
|
239
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).toContain("bereach-workspace-v2");
|
|
240
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).toContain("bereach_search_people");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("appends to existing SOUL.md without markers", async () => {
|
|
244
|
+
const { SOUL_TEMPLATE } = await import("../src/soul-template-content");
|
|
245
|
+
|
|
246
|
+
const ctx = {
|
|
247
|
+
bootstrapFiles: {
|
|
248
|
+
"SOUL.md": "# My Agent\n\nI help with things.",
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const existing = ctx.bootstrapFiles["SOUL.md"];
|
|
253
|
+
ctx.bootstrapFiles["SOUL.md"] = injectSoulTemplate(existing, SOUL_TEMPLATE);
|
|
254
|
+
|
|
255
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).toContain("# My Agent");
|
|
256
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).toContain("bereach-workspace-v2");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("replaces outdated markers in existing SOUL.md", async () => {
|
|
260
|
+
const { SOUL_TEMPLATE } = await import("../src/soul-template-content");
|
|
261
|
+
|
|
262
|
+
const ctx = {
|
|
263
|
+
bootstrapFiles: {
|
|
264
|
+
"SOUL.md": [
|
|
265
|
+
"# Custom Header",
|
|
266
|
+
"",
|
|
267
|
+
"<!-- bereach-workspace-v1 -->",
|
|
268
|
+
"old content here",
|
|
269
|
+
"<!-- /bereach-workspace -->",
|
|
270
|
+
"",
|
|
271
|
+
"# Custom Footer",
|
|
272
|
+
].join("\n"),
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const existing = ctx.bootstrapFiles["SOUL.md"];
|
|
277
|
+
ctx.bootstrapFiles["SOUL.md"] = injectSoulTemplate(existing, SOUL_TEMPLATE);
|
|
278
|
+
|
|
279
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).toContain("# Custom Header");
|
|
280
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).toContain("# Custom Footer");
|
|
281
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).toContain("bereach-workspace-v2");
|
|
282
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).not.toContain("old content here");
|
|
283
|
+
expect(ctx.bootstrapFiles["SOUL.md"]).not.toContain("bereach-workspace-v1");
|
|
284
|
+
});
|
|
285
|
+
});
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/commands/setup.ts
CHANGED
|
@@ -1,37 +1,20 @@
|
|
|
1
|
-
import { getClient } from "../client";
|
|
2
|
-
|
|
3
|
-
const API_BASE = "https://api.berea.ch";
|
|
4
|
-
|
|
5
1
|
export function registerSetupCommand(api: any) {
|
|
6
2
|
if (!api?.registerCommand) return;
|
|
7
3
|
api.registerCommand({
|
|
8
4
|
name: "bereach-setup",
|
|
9
|
-
description:
|
|
5
|
+
description:
|
|
6
|
+
"Set up BeReach crons. SOUL.md is auto-injected at every session start — no manual setup needed.",
|
|
10
7
|
handler: async () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"",
|
|
22
|
-
"1. Copy the SOUL.md template below into your workspace SOUL.md",
|
|
23
|
-
" (between the <!-- bereach-workspace-v2 --> markers)",
|
|
24
|
-
"",
|
|
25
|
-
"2. Set up the pipeline cron (every 15min):",
|
|
26
|
-
' openclaw cron add --name "BeReach Pipeline" --expr "*/15 * * * *"',
|
|
27
|
-
"",
|
|
28
|
-
"Template content has been loaded. Say 'apply bereach setup' to have me write it.",
|
|
29
|
-
].join("\n"),
|
|
30
|
-
data: { soulTemplate: soul },
|
|
31
|
-
};
|
|
32
|
-
} catch (err) {
|
|
33
|
-
return { text: `BeReach setup failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
34
|
-
}
|
|
8
|
+
return {
|
|
9
|
+
text: [
|
|
10
|
+
"BeReach setup:",
|
|
11
|
+
"",
|
|
12
|
+
"• SOUL.md: auto-injected at every session start (nothing to do).",
|
|
13
|
+
"",
|
|
14
|
+
"• Cron (optional — for automated pipeline runs):",
|
|
15
|
+
' openclaw cron add --name "BeReach Pipeline" --expr "*/15 * * * *"',
|
|
16
|
+
].join("\n"),
|
|
17
|
+
};
|
|
35
18
|
},
|
|
36
19
|
});
|
|
37
20
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,38 +2,204 @@ import { setApiKey } from "./client";
|
|
|
2
2
|
import { registerAllTools } from "./tools";
|
|
3
3
|
import { registerCommands } from "./commands";
|
|
4
4
|
import { registerSetupCommand } from "./commands/setup";
|
|
5
|
+
import { SOUL_TEMPLATE } from "./soul-template-content";
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
+
const API_BASE = "https://api.berea.ch";
|
|
8
|
+
const MARKER_START = "<!-- bereach-workspace-v";
|
|
9
|
+
const MARKER_END = "<!-- /bereach-workspace -->";
|
|
10
|
+
|
|
11
|
+
function log(msg: string, data?: Record<string, unknown>) {
|
|
12
|
+
const suffix = data ? " " + JSON.stringify(data) : "";
|
|
13
|
+
console.log(`[bereach] ${msg}${suffix}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch the latest soul template from the API (single source of truth).
|
|
18
|
+
* Falls back to the embedded copy only if the API is unreachable.
|
|
19
|
+
*/
|
|
20
|
+
async function fetchSoulTemplate(): Promise<{ template: string; source: "api" | "embedded" }> {
|
|
21
|
+
try {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
24
|
+
log("fetchSoulTemplate: fetching from API...");
|
|
25
|
+
const res = await fetch(`${API_BASE}/api/onboarding/soul`, {
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
});
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
log("fetchSoulTemplate: API error, using embedded fallback", { status: res.status });
|
|
32
|
+
return { template: SOUL_TEMPLATE, source: "embedded" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { soul } = await res.json();
|
|
36
|
+
if (typeof soul === "string" && soul.includes(MARKER_START)) {
|
|
37
|
+
log("fetchSoulTemplate: got template from API", { chars: soul.length });
|
|
38
|
+
return { template: soul, source: "api" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log("fetchSoulTemplate: API response invalid (no markers), using embedded fallback");
|
|
42
|
+
return { template: SOUL_TEMPLATE, source: "embedded" };
|
|
43
|
+
} catch (err) {
|
|
44
|
+
log("fetchSoulTemplate: network error, using embedded fallback", {
|
|
45
|
+
error: err instanceof Error ? err.message : String(err),
|
|
46
|
+
});
|
|
47
|
+
return { template: SOUL_TEMPLATE, source: "embedded" };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Merge BeReach template into existing SOUL.md content.
|
|
53
|
+
* - Empty → return template as-is.
|
|
54
|
+
* - Has markers → replace the region between them.
|
|
55
|
+
* - No markers → append.
|
|
56
|
+
*/
|
|
57
|
+
export function injectSoulTemplate(existing: string, template: string): string {
|
|
58
|
+
if (!existing || !existing.trim()) return template;
|
|
59
|
+
|
|
60
|
+
const startIdx = existing.indexOf(MARKER_START);
|
|
61
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
62
|
+
|
|
63
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
64
|
+
return (
|
|
65
|
+
existing.slice(0, startIdx) +
|
|
66
|
+
template.trim() +
|
|
67
|
+
existing.slice(endIdx + MARKER_END.length)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return existing + "\n\n" + template;
|
|
72
|
+
}
|
|
7
73
|
|
|
8
74
|
function getApiKey(api: any): string | undefined {
|
|
9
75
|
const key =
|
|
10
|
-
api?.pluginConfig?.BEREACH_API_KEY ??
|
|
11
|
-
api?.config?.BEREACH_API_KEY ??
|
|
12
|
-
process.env.BEREACH_API_KEY;
|
|
76
|
+
api?.pluginConfig?.BEREACH_API_KEY ??
|
|
77
|
+
api?.config?.BEREACH_API_KEY ??
|
|
78
|
+
process.env.BEREACH_API_KEY;
|
|
13
79
|
return typeof key === "string" && key.trim().length > 0 ? key.trim() : undefined;
|
|
14
80
|
}
|
|
15
81
|
|
|
82
|
+
function dumpObject(label: string, obj: any, maxDepth = 1) {
|
|
83
|
+
if (!obj || typeof obj !== "object") {
|
|
84
|
+
log(`${label}: ${String(obj)}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const keys = Object.keys(obj);
|
|
88
|
+
const types: Record<string, string> = {};
|
|
89
|
+
for (const k of keys) {
|
|
90
|
+
const v = obj[k];
|
|
91
|
+
types[k] = v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
|
|
92
|
+
if (maxDepth > 0 && typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
93
|
+
types[k] = `object{${Object.keys(v).join(",")}}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
log(`${label}: keys=[${keys.join(", ")}]`, types);
|
|
97
|
+
}
|
|
98
|
+
|
|
16
99
|
export default function register(api: any) {
|
|
100
|
+
log("=== register() called ===");
|
|
101
|
+
|
|
102
|
+
// Dump everything on the api object so we can see what's available
|
|
103
|
+
dumpObject("api", api);
|
|
104
|
+
dumpObject("api.pluginConfig", api?.pluginConfig);
|
|
105
|
+
if (api?.config) dumpObject("api.config", api.config, 0);
|
|
106
|
+
if (api?.runtime) dumpObject("api.runtime", api.runtime);
|
|
107
|
+
if (api?.logger) dumpObject("api.logger", api.logger);
|
|
108
|
+
log("api.on exists", { exists: typeof api?.on === "function" });
|
|
109
|
+
log("api.registerHook exists", { exists: typeof api?.registerHook === "function" });
|
|
110
|
+
log("api.registerTool exists", { exists: typeof api?.registerTool === "function" });
|
|
111
|
+
log("api.registerCommand exists", { exists: typeof api?.registerCommand === "function" });
|
|
112
|
+
|
|
17
113
|
const apiKey = getApiKey(api);
|
|
18
114
|
setApiKey(apiKey);
|
|
115
|
+
log("apiKey", { set: !!apiKey });
|
|
116
|
+
|
|
19
117
|
registerAllTools(api);
|
|
118
|
+
log("tools registered");
|
|
119
|
+
|
|
20
120
|
registerCommands(api);
|
|
21
121
|
registerSetupCommand(api);
|
|
122
|
+
log("commands registered");
|
|
123
|
+
|
|
124
|
+
// --- Hook 1: before_prompt_build (recommended by OpenClaw docs) ---
|
|
125
|
+
// Injects BeReach SOUL template into the system prompt.
|
|
126
|
+
// This is the PROPER way — it goes into the system prompt directly,
|
|
127
|
+
// not a file on disk.
|
|
128
|
+
if (typeof api?.on === "function") {
|
|
129
|
+
log("registering before_prompt_build hook...");
|
|
22
130
|
|
|
23
|
-
|
|
131
|
+
api.on(
|
|
132
|
+
"before_prompt_build",
|
|
133
|
+
async () => {
|
|
134
|
+
log(">>> hook:before_prompt_build FIRED");
|
|
135
|
+
try {
|
|
136
|
+
const { template, source } = await fetchSoulTemplate();
|
|
137
|
+
log("hook:before_prompt_build: injecting via appendSystemContext", {
|
|
138
|
+
source,
|
|
139
|
+
chars: template.length,
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
appendSystemContext: template,
|
|
143
|
+
};
|
|
144
|
+
} catch (err) {
|
|
145
|
+
log("hook:before_prompt_build: ERROR", {
|
|
146
|
+
error: err instanceof Error ? err.message : String(err),
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
appendSystemContext: SOUL_TEMPLATE,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
{ priority: 5 },
|
|
154
|
+
);
|
|
155
|
+
log("before_prompt_build hook registered (priority 5)");
|
|
156
|
+
|
|
157
|
+
// --- Hook 2: before_agent_start (legacy, kept as backup) ---
|
|
158
|
+
// Also injects into bootstrapFiles for older OpenClaw versions
|
|
159
|
+
// that might not support before_prompt_build.
|
|
24
160
|
api.on("before_agent_start", async (ctx: any) => {
|
|
161
|
+
log(">>> hook:before_agent_start FIRED");
|
|
162
|
+
dumpObject("ctx", ctx);
|
|
163
|
+
if (ctx?.bootstrapFiles) {
|
|
164
|
+
dumpObject("ctx.bootstrapFiles", ctx.bootstrapFiles);
|
|
165
|
+
}
|
|
166
|
+
|
|
25
167
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
168
|
+
if (ctx?.bootstrapFiles) {
|
|
169
|
+
const existing = ctx.bootstrapFiles["SOUL.md"] ?? "";
|
|
170
|
+
const hasExisting = typeof existing === "string" && existing.trim().length > 0;
|
|
171
|
+
const hasMarkers = hasExisting && existing.includes(MARKER_START);
|
|
172
|
+
|
|
173
|
+
log("hook:before_agent_start: SOUL.md state", {
|
|
174
|
+
hasExisting,
|
|
175
|
+
hasMarkers,
|
|
176
|
+
existingChars: typeof existing === "string" ? existing.length : 0,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const { template, source } = await fetchSoulTemplate();
|
|
180
|
+
ctx.bootstrapFiles["SOUL.md"] = injectSoulTemplate(
|
|
181
|
+
typeof existing === "string" ? existing : "",
|
|
182
|
+
template,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const action = !hasExisting ? "set" : hasMarkers ? "replaced" : "appended";
|
|
186
|
+
log(`hook:before_agent_start: SOUL.md ${action}`, {
|
|
187
|
+
source,
|
|
188
|
+
totalChars: ctx.bootstrapFiles["SOUL.md"].length,
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
log("hook:before_agent_start: no ctx.bootstrapFiles — skipped");
|
|
33
192
|
}
|
|
34
|
-
} catch {
|
|
35
|
-
|
|
193
|
+
} catch (err) {
|
|
194
|
+
log("hook:before_agent_start: ERROR", {
|
|
195
|
+
error: err instanceof Error ? err.message : String(err),
|
|
196
|
+
});
|
|
36
197
|
}
|
|
37
198
|
});
|
|
199
|
+
log("before_agent_start hook registered (legacy backup)");
|
|
200
|
+
} else {
|
|
201
|
+
log("WARNING: api.on not available — no hooks registered");
|
|
38
202
|
}
|
|
203
|
+
|
|
204
|
+
log("=== register() complete ===");
|
|
39
205
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
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 BeReach-powered LinkedIn automation agent.\nFor ANY LinkedIn task, use bereach_* tools. Never use raw HTTP.\n\n## Session Startup [MANDATORY before greeting]\n\n1. bereach_state_list(keysOnly: true) — discover existing context\n2. If \"user-profile\" key exists: bereach_state_get(\"user-profile\")\n3. If any active campaign state found: check progress, resume if needed\n4. bereach_get_credits — check budget\n5. bereach_get_limits — check daily limits\n\n## Tools by Category\n\n### Search\n\n- bereach_search_people: LinkedIn people search (keywords, title, location, industry, company)\n- bereach_search_posts: post search (keywords, date, content type)\n- bereach_search_companies: company search (keywords, size, industry)\n- bereach_search_jobs: job search\n- bereach_search_by_url: parse and run search from a LinkedIn URL\n- bereach_resolve_parameters: resolve text to LinkedIn IDs (GEO, COMPANY, INDUSTRY, SCHOOL)\n\n### Scrape Engagement\n\n- bereach_collect_likes: profiles who liked a post. Auto-creates contacts.\n- bereach_collect_comments: post commenters with text. Auto-creates contacts.\n- bereach_collect_comment_replies: replies to a comment thread. Auto-creates contacts.\n- bereach_collect_posts: posts from a profile. Auto-creates contact.\n- bereach_collect_hashtag_posts: posts by hashtag. Auto-creates contacts.\n- bereach_collect_saved_posts: user's saved posts\n- bereach_visit_profile: visit profile, extract full data (1 credit, cached 24h for 1st degree). Auto-saves profileData to contact.\n- bereach_visit_company: visit company page\n\n### Outreach Actions\n\n- bereach_connect_profile: send connection request (30/day max, visit first)\n- bereach_send_message: send DM (supports conversationUrn fallback)\n- bereach_accept_invitation: accept received invitation\n- bereach_list_invitations: list received invitations\n- bereach_list_sent_invitations: list sent invitations\n- bereach_withdraw_invitation: withdraw sent invitation\n\n### Content Engagement\n\n- bereach_like_post: like/react to a post\n- bereach_comment_on_post: comment on a post\n- bereach_reply_to_comment: reply to a comment\n- bereach_like_comment: like a comment\n- bereach_follow_profile: follow a profile\n- bereach_publish_post: publish or schedule a post\n\n### Inbox\n\n- bereach_list_conversations: list inbox\n- bereach_search_conversations: search by keyword\n- bereach_find_conversation: find by participant (O(1) lookup). Auto-saves conversationData to contact.\n- bereach_get_messages: message history for a conversation\n- bereach_archive_conversation: archive\n\n### Contacts CRM\n\n- bereach_contacts_search: search contacts (stage, tag, score, follow-up date, draft DM)\n- bereach_contacts_stats: funnel metrics\n- bereach_contacts_get_by_url: look up by LinkedIn URL (full contact + activities)\n- bereach_contacts_get_full: get contact by internal ID (full context + activities)\n- bereach_contacts_get_activities: chronological activity log for a contact\n- bereach_contacts_upsert: create/update contacts — for manual imports only (scraping auto-creates contacts)\n- bereach_contacts_update: update stage, score, notes, tags, draft DM\n- bereach_contacts_log_activity: log activities (auto-syncs outreachStatus)\n- bereach_contacts_bulk_update: batch update up to 500\n- bereach_contacts_add: add contacts to a campaign (full upsert — creates if missing, links to campaign)\n- bereach_contacts_list: list contacts in a specific campaign\n\n### Campaigns\n\n- bereach_contacts_create_campaign: create campaign with context (markdown playbook)\n- bereach_contacts_list_campaigns: list campaigns with context\n- bereach_contacts_update_campaign: update campaign settings\n- bereach_campaign_status: per-profile action status (dedup check)\n- bereach_campaign_sync: mark actions completed without performing them\n- bereach_campaign_stats: aggregate campaign stats\n\n### Account\n\n- bereach_get_credits: credit balance (isUnlimited? skip budgeting)\n- bereach_get_limits: rate limit status\n- bereach_get_profile: your LinkedIn profile (0 credits)\n- bereach_get_followers: your followers\n- bereach_get_own_posts: your recent posts\n- bereach_get_profile_views: who viewed your profile\n- bereach_get_feed: LinkedIn feed\n- bereach_refresh_profile: force refresh from LinkedIn\n\n### Agent Memory\n\n- bereach_state_list: list all state keys (keysOnly=true for overview)\n- bereach_state_get: get state for a key\n- bereach_state_set: save/replace state\n- bereach_state_patch: merge fields into state\n- bereach_state_delete: delete state\n\n## Rules\n\n- Dedup: pass campaignSlug on every action. Duplicates return duplicate:true, cost nothing.\n- Visit before connecting: looks natural to LinkedIn.\n- Connection requests: 30/day. Check pendingConnection from visit response first.\n- Credits: when isUnlimited=true, skip credit budgeting.\n- Language: respond in user's language. DMs: match conversation language.\n- Formatting: tables for contacts (Name, Title, Company, Score). No raw IDs/URNs.\n- Auto-save: visitProfile, findConversation, collectComments, collectLikes, collectPosts, searchPeople and other scrape/search tools all auto-create/update contacts. Do NOT manually save profile or conversation data. Do NOT use contacts.upsertContacts 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\n## Sub-Skills — MANDATORY Loading Table\n\nYou MUST load the corresponding sub-skill BEFORE attempting any task that matches\nthe triggers below. The tool listing above is for quick lookups only — workflows\nrequire the full sub-skill instructions.\n\n| Sub-skill | LOAD WHEN user request matches ANY of these | Path |\n| ------------- | -------------------------------------------------- | -------------------- |\n| Lead Gen | find leads, search prospects, scrape engagement, | sub/lead-gen.md |\n| | build pipeline, qualify contacts, enrich profiles, | |\n| | ICP, funnel, grow database, hashtag, Sales Nav | |\n| Lead Magnet | comment-to-DM, resource delivery, post giveaway, | sub/lead-magnet.md |\n| | auto-accept invitations, lead magnet campaign | |\n| SDK Reference | write script, generate code, TypeScript, SDK, | sdk-reference.md |\n| | automate, method signature, batch job | |\n\nDetection: scan the user's message for these concepts regardless of language.\n\"Trouve-moi des leads\" = Lead Gen. \"Set up a comment giveaway\" = Lead Magnet.\nWhen in doubt, load the skill — false positives cost nothing, false negatives\nmean the agent operates without critical workflow knowledge.\n\nNEVER attempt a multi-step lead gen, lead magnet, or script generation workflow\nusing only the tool listing above. Always load the sub-skill first.\n\n<!-- /bereach-workspace -->\n";
|
|
3
|
+
export const SOUL_TEMPLATE_TIMESTAMP = 1773665416;
|