bereach-openclaw 1.5.8 → 1.5.10

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.
Files changed (34) hide show
  1. package/node_modules/@bereach/tools/package.json +4 -1
  2. package/node_modules/@bereach/tools/src/api-client.ts +158 -0
  3. package/node_modules/@bereach/tools/src/cost-estimation.ts +69 -0
  4. package/node_modules/@bereach/tools/src/definitions.ts +0 -12
  5. package/node_modules/@bereach/tools/src/enforcement-types.ts +1 -27
  6. package/node_modules/@bereach/tools/src/index.ts +16 -4
  7. package/node_modules/@bereach/tools/src/llm-errors.ts +24 -0
  8. package/node_modules/@bereach/tools/src/parse-utils.ts +6 -47
  9. package/node_modules/@bereach/tools/src/utils.ts +9 -0
  10. package/openclaw.plugin.json +1 -1
  11. package/package.json +5 -5
  12. package/skills/bereach/SKILL.md +2 -2
  13. package/skills/bereach/sdk-reference.md +1 -10
  14. package/src/commands/connector/api.ts +113 -0
  15. package/src/commands/connector/execution.ts +139 -0
  16. package/src/commands/connector/index.ts +388 -0
  17. package/src/commands/connector/types.ts +79 -0
  18. package/src/commands/setup.ts +3 -2
  19. package/src/connector/manager.ts +11 -18
  20. package/src/connector-cli.ts +1 -1
  21. package/src/hooks/context/formatters.ts +584 -0
  22. package/src/hooks/context/index.ts +311 -0
  23. package/src/hooks/context/task-context.ts +79 -0
  24. package/src/hooks/enforcement.ts +11 -100
  25. package/src/hooks/lifecycle.ts +84 -117
  26. package/src/hooks/tracking.ts +11 -10
  27. package/src/hooks/types.ts +0 -2
  28. package/src/hooks/utils.ts +1 -0
  29. package/src/index.ts +61 -23
  30. package/src/tools/index.ts +8 -117
  31. package/src/commands/connector.ts +0 -997
  32. package/src/hooks/context.ts +0 -1133
  33. package/src/hooks/enforcement-rules.ts +0 -5
  34. package/src/tools/definitions.ts +0 -9
@@ -11,7 +11,10 @@
11
11
  "./utils": "./src/utils.ts",
12
12
  "./cache-types": "./src/cache-types.ts",
13
13
  "./parse-utils": "./src/parse-utils.ts",
14
- "./task-tool-whitelist": "./src/task-tool-whitelist.ts"
14
+ "./task-tool-whitelist": "./src/task-tool-whitelist.ts",
15
+ "./api-client": "./src/api-client.ts",
16
+ "./cost-estimation": "./src/cost-estimation.ts",
17
+ "./llm-errors": "./src/llm-errors.ts"
15
18
  },
16
19
  "files": ["src/"],
17
20
  "scripts": {
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Shared HTTP client for BeReach API calls.
3
+ * Handles retries, rate limiting, auth errors, and parameter interpolation.
4
+ *
5
+ * Used by both OpenClaw plugin and MCP server.
6
+ * The caller provides apiBase and apiKey — no build-time placeholders needed.
7
+ */
8
+
9
+ import type { ToolDefinition } from "./types";
10
+
11
+ const MAX_RETRIES = 2;
12
+ const TIMEOUT_MS = 8000;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // URL building
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Interpolate `{param}` placeholders in apiPath, returning the resolved path
20
+ * and the remaining body params (path params removed).
21
+ */
22
+ function buildPath(
23
+ apiPath: string,
24
+ params: Record<string, unknown>,
25
+ ): { path: string; bodyParams: Record<string, unknown> } {
26
+ const bodyParams = { ...params };
27
+ const path = apiPath.replace(/\{(\w+)\}/g, (_, name) => {
28
+ const val = bodyParams[name];
29
+ if (val === undefined || val === null || val === "") {
30
+ throw new Error(`Missing required parameter: ${name} for ${apiPath}`);
31
+ }
32
+ delete bodyParams[name];
33
+ return encodeURIComponent(String(val));
34
+ });
35
+ return { path, bodyParams };
36
+ }
37
+
38
+ function buildUrl(
39
+ apiBase: string,
40
+ path: string,
41
+ method: string,
42
+ bodyParams: Record<string, unknown>,
43
+ ): string {
44
+ let url = `${apiBase}${path}`;
45
+ if (method === "GET" || method === "DELETE") {
46
+ const qs = new URLSearchParams();
47
+ for (const [k, v] of Object.entries(bodyParams)) {
48
+ if (v !== undefined && v !== null) qs.set(k, String(v));
49
+ }
50
+ const qsStr = qs.toString();
51
+ if (qsStr) url += `?${qsStr}`;
52
+ }
53
+ return url;
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Response handling
58
+ // ---------------------------------------------------------------------------
59
+
60
+ function isReadOnly(method: string): boolean {
61
+ return method === "GET" || method === "DELETE";
62
+ }
63
+
64
+ /** Handle non-OK responses. Returns a wait-ms for 429, or throws (after reading body). */
65
+ async function handleErrorResponse(resp: Response, path: string, method: string, attempt: number): Promise<number> {
66
+ if (resp.status === 429) {
67
+ const retryAfter = resp.headers.get("retry-after");
68
+ const parsed = retryAfter ? parseInt(retryAfter, 10) : NaN;
69
+ return Number.isFinite(parsed) && parsed > 0
70
+ ? parsed * 1000
71
+ : backoffMs(attempt);
72
+ }
73
+ const errorBody = await resp.text().catch(() => "");
74
+ if (resp.status === 401) {
75
+ throw new Error(
76
+ "BeReach API key is invalid or expired (401 Unauthorized). " +
77
+ "The user should get a valid key at https://bereach.ai/token and set it with: /bereach-set-key brc_NEW_KEY",
78
+ );
79
+ }
80
+ throw new Error(`API ${method} ${path} failed (${resp.status}): ${errorBody}`);
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Retry logic
85
+ // ---------------------------------------------------------------------------
86
+
87
+ function isTimeout(err: Error): boolean {
88
+ const m = err.message;
89
+ return m.includes("timed out") || m.includes("timeout") || m.includes("abort");
90
+ }
91
+
92
+ function isRetryable(err: Error): boolean {
93
+ const m = err.message;
94
+ // Non-retryable: explicit API failures and rate-limit errors we already handled
95
+ return !m.includes("failed (") && !m.includes("Rate limited");
96
+ }
97
+
98
+ function backoffMs(attempt: number): number {
99
+ return Math.pow(2, attempt + 1) * 1000;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Main entry point
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Execute an API call for a tool definition.
108
+ * Interpolates path parameters, handles GET/POST, retries on transient failures.
109
+ */
110
+ export async function executeApiCall(
111
+ def: ToolDefinition,
112
+ params: Record<string, unknown>,
113
+ apiKey: string,
114
+ apiBase: string,
115
+ ): Promise<unknown> {
116
+ const method = def.apiMethod ?? "POST";
117
+ const { path, bodyParams } = buildPath(def.apiPath!, params);
118
+ const url = buildUrl(apiBase, path, method, bodyParams);
119
+
120
+ let lastError: Error | null = null;
121
+
122
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
123
+ try {
124
+ const resp = await fetch(url, {
125
+ method,
126
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
127
+ ...(!isReadOnly(method) ? { body: JSON.stringify(bodyParams) } : {}),
128
+ signal: AbortSignal.timeout(TIMEOUT_MS),
129
+ });
130
+
131
+ if (!resp.ok) {
132
+ const waitMs = await handleErrorResponse(resp, path, method, attempt);
133
+ // 429: retry if attempts remain, otherwise throw
134
+ if (attempt < MAX_RETRIES) {
135
+ await new Promise((r) => setTimeout(r, waitMs));
136
+ continue;
137
+ }
138
+ const body = await resp.text().catch(() => "");
139
+ throw new Error(`Rate limited on ${path}. ${body}`);
140
+ }
141
+
142
+ return resp.json();
143
+ } catch (err) {
144
+ lastError = err instanceof Error ? err : new Error(String(err));
145
+
146
+ // Don't retry write ops on timeout — the action may have succeeded server-side
147
+ if (!isReadOnly(method) && isTimeout(lastError)) throw lastError;
148
+
149
+ if (attempt < MAX_RETRIES && isRetryable(lastError)) {
150
+ await new Promise((r) => setTimeout(r, backoffMs(attempt)));
151
+ continue;
152
+ }
153
+ throw lastError;
154
+ }
155
+ }
156
+
157
+ throw lastError ?? new Error(`Failed after ${MAX_RETRIES + 1} attempts`);
158
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * LLM cost estimation — model pricing and token usage extraction.
3
+ * Single source of truth, used by both lifecycle hook and connector.
4
+ */
5
+
6
+ /** Model pricing per 1M tokens */
7
+ export const MODEL_PRICING: Record<string, { input: number; output: number; cacheRead: number }> = {
8
+ "haiku": { input: 1.0, output: 5.0, cacheRead: 0.1 },
9
+ "sonnet": { input: 3.0, output: 15.0, cacheRead: 0.3 },
10
+ "flash": { input: 0.3, output: 2.5, cacheRead: 0.03 },
11
+ "pro": { input: 1.25, output: 10.0, cacheRead: 0.125 },
12
+ };
13
+
14
+ export type TokenUsage = {
15
+ inputTokens: number;
16
+ outputTokens: number;
17
+ cacheReadTokens?: number;
18
+ cacheWriteTokens?: number;
19
+ };
20
+
21
+ /**
22
+ * Estimate cost in USD from token usage.
23
+ * Matches model by substring (e.g. "claude-3-5-haiku" matches "haiku").
24
+ */
25
+ export function estimateTaskCost(
26
+ inputTokens: number,
27
+ outputTokens: number,
28
+ cacheReadTokens: number,
29
+ modelSlug?: string | null,
30
+ ): number {
31
+ const key = Object.keys(MODEL_PRICING).find((k) => modelSlug?.includes(k)) ?? "haiku";
32
+ const p = MODEL_PRICING[key];
33
+ const uncached = Math.max(0, inputTokens - cacheReadTokens);
34
+ return (uncached * p.input + cacheReadTokens * p.cacheRead + outputTokens * p.output) / 1_000_000;
35
+ }
36
+
37
+ /**
38
+ * Extract token usage from an OpenClaw meta/wrapper object.
39
+ * Handles multiple data locations across OpenClaw versions:
40
+ * - meta.agentMeta.lastCallUsage (OpenClaw 2026.4+)
41
+ * - meta.cost (older versions / test runner)
42
+ * - meta.usage (alternative format)
43
+ */
44
+ export function extractTokenUsage(
45
+ meta: Record<string, unknown>,
46
+ ): { usage: TokenUsage; model?: string } | null {
47
+ const agentMeta = (meta.agentMeta ?? {}) as Record<string, unknown>;
48
+ const lastCall = (agentMeta.lastCallUsage ?? {}) as Record<string, number>;
49
+ const costData = (meta.cost ?? {}) as Record<string, number>;
50
+ const usageData = (meta.usage ?? {}) as Record<string, number>;
51
+
52
+ const inputTokens = lastCall.input ?? costData.inputTokens ?? costData.input ?? usageData.input ?? 0;
53
+ const outputTokens = lastCall.output ?? costData.outputTokens ?? costData.output ?? usageData.output ?? 0;
54
+
55
+ if (inputTokens === 0 && outputTokens === 0) return null;
56
+
57
+ const cacheRead = lastCall.cacheRead ?? usageData.cacheRead ?? costData.cacheReadTokens ?? 0;
58
+ const cacheWrite = lastCall.cacheWrite ?? usageData.cacheWrite ?? costData.cacheWriteTokens ?? 0;
59
+
60
+ return {
61
+ usage: {
62
+ inputTokens,
63
+ outputTokens,
64
+ ...(cacheRead > 0 ? { cacheReadTokens: cacheRead } : {}),
65
+ ...(cacheWrite > 0 ? { cacheWriteTokens: cacheWrite } : {}),
66
+ },
67
+ model: agentMeta.model as string | undefined,
68
+ };
69
+ }
@@ -2055,18 +2055,6 @@ export const definitions: ToolDefinition[] = [
2055
2055
  },
2056
2056
  },
2057
2057
 
2058
- // ── Self Upgrade ────────────────────────────────────────────────
2059
-
2060
- {
2061
- name: "bereach_self_upgrade",
2062
- description: "Upgrade the BeReach plugin to the latest version. Runs npm install and requires a session restart afterward.",
2063
- handler: "__self_upgrade__",
2064
- parameters: {
2065
- type: "object",
2066
- properties: {},
2067
- },
2068
- },
2069
-
2070
2058
  // ── API Key Setup ────────────────────────────────────────────────
2071
2059
 
2072
2060
  {
@@ -123,7 +123,6 @@ export const FREE_TOOLS = new Set([
123
123
  "bereach_scheduled_message_cancel",
124
124
  "bereach_draft_schedule",
125
125
  "bereach_switch_account",
126
- "bereach_self_upgrade",
127
126
  "bereach_get_dm_history",
128
127
  "bereach_get_conversation_summary",
129
128
  "bereach_save_conversation_summary",
@@ -171,7 +170,7 @@ export const PACING = {
171
170
  export const DEFAULTS = {
172
171
  maxVisitsPerSession: 200,
173
172
  engagementThreshold: 5,
174
- contextCacheTtlMs: 5 * 60 * 1000, // 5 minutes
173
+ contextCacheTtlMs: 15 * 60 * 1000, // 15 minutes
175
174
  maxResultItems: 15,
176
175
  maxPostTextLength: 500,
177
176
  } as const;
@@ -275,31 +274,6 @@ export function createSessionState(): SessionState {
275
274
  };
276
275
  }
277
276
 
278
- // ---------------------------------------------------------------------------
279
- // Upgrade classification
280
- // ---------------------------------------------------------------------------
281
-
282
- export type UpgradeKind = "none" | "patch" | "minor" | "major";
283
-
284
- /** Compare two semver strings and return the upgrade kind. Strips prerelease tags. */
285
- export function classifyUpgrade(current: string, latest: string): UpgradeKind {
286
- const parse = (v: string) => {
287
- const cleaned = v.replace(/^v/i, "");
288
- const [main, pre] = cleaned.split("-", 2);
289
- const [major, minor, patch] = (main ?? "0.0.0").split(".").map(Number);
290
- return { major: major || 0, minor: minor || 0, patch: patch || 0, pre: pre ?? "" };
291
- };
292
- const c = parse(current);
293
- const l = parse(latest);
294
- if (l.major !== c.major) return l.major > c.major ? "major" : "none";
295
- if (l.minor !== c.minor) return l.minor > c.minor ? "minor" : "none";
296
- if (l.patch !== c.patch) return l.patch > c.patch ? "patch" : "none";
297
- // Same base version — prerelease-to-prerelease or prerelease-to-stable
298
- if (c.pre && !l.pre) return "patch"; // beta → stable release of same version
299
- if (c.pre && l.pre && l.pre !== c.pre) return "patch"; // beta.12 → beta.15
300
- return "none";
301
- }
302
-
303
277
  // ---------------------------------------------------------------------------
304
278
  // Campaign types — DB is the single source of truth
305
279
  // ---------------------------------------------------------------------------
@@ -7,9 +7,9 @@ export {
7
7
  READ_TOOLS, WRITE_TOOLS, FREE_TOOLS, PACED_FREE_TOOLS,
8
8
  VISIT_FIRST_TOOLS, PROFILE_TARGETING_TOOLS,
9
9
  PACING, DEFAULTS,
10
- createSessionState, classifyUpgrade,
10
+ createSessionState,
11
11
  type PluginConfig, type SessionState, type TaskModeInfo,
12
- type DbCampaign, type UpgradeKind,
12
+ type DbCampaign,
13
13
  } from "./enforcement-types.js";
14
14
 
15
15
  // Pure enforcement rules
@@ -23,6 +23,7 @@ export {
23
23
 
24
24
  // Utilities (URL normalization, profile extraction, result parsing)
25
25
  export {
26
+ errMsg,
26
27
  createLogger, normalizeUrl, extractProfile, extractDmIdentifier,
27
28
  extractToolResult, isErrorResult,
28
29
  } from "./utils.js";
@@ -33,13 +34,24 @@ export type {
33
34
  CachedAccount, SessionMeta, OnboardingState, RecentEvent, CacheStore,
34
35
  } from "./cache-types.js";
35
36
 
36
- // Parse utilities (semver, structured result parsing)
37
+ // Parse utilities (structured result parsing)
37
38
  export {
38
39
  extractTextFromContent, parseStructuredResult,
39
- parseSemver, semverGt,
40
40
  } from "./parse-utils.js";
41
41
 
42
42
  // Task tool whitelists (per-task-type tool pruning)
43
43
  export {
44
44
  getTaskToolWhitelist, getTaskToolNames,
45
45
  } from "./task-tool-whitelist.js";
46
+
47
+ // API client (HTTP call execution with retry, rate limiting)
48
+ export { executeApiCall } from "./api-client.js";
49
+
50
+ // Cost estimation (model pricing, token usage extraction)
51
+ export {
52
+ MODEL_PRICING, estimateTaskCost, extractTokenUsage,
53
+ type TokenUsage,
54
+ } from "./cost-estimation.js";
55
+
56
+ // LLM error detection
57
+ export { detectLlmError } from "./llm-errors.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * LLM provider error detection patterns.
3
+ * Mirrors apps/web/src/lib/errors/llm-errors.ts but self-contained
4
+ * (shared package has zero dependencies on apps/).
5
+ *
6
+ * Used by connector to detect auth/billing failures from agent output.
7
+ */
8
+
9
+ const LLM_ERROR_PATTERNS = [
10
+ /authentication_error/i, /invalid.*api.?key/i, /permission_error/i,
11
+ /401.*unauthorized/i, /access_denied.*api/i, /unauthorized.*invalid/i,
12
+ /out of (extra )?usage/i, /insufficient.quota/i, /exceeded.*current quota/i,
13
+ /RESOURCE_EXHAUSTED/i, /credit balance.*too low/i, /exceeded.*spending.*limit/i,
14
+ /billing.*hard.*limit/i, /payment.*required/i,
15
+ ];
16
+
17
+ /** Scan text for LLM provider errors (auth, billing). Returns the matched text or null. */
18
+ export function detectLlmError(text: string): string | null {
19
+ for (const pattern of LLM_ERROR_PATTERNS) {
20
+ const match = text.match(pattern);
21
+ if (match) return match[0];
22
+ }
23
+ return null;
24
+ }
@@ -21,11 +21,16 @@ export function extractTextFromContent(content: unknown): string {
21
21
  const obj = content as Record<string, unknown>;
22
22
  if (typeof obj.text === "string") return obj.text;
23
23
  if (typeof obj.value === "string") return obj.value;
24
+ // Last resort: stringify
24
25
  return JSON.stringify(content);
25
26
  }
26
27
  return "";
27
28
  }
28
29
 
30
+ // ---------------------------------------------------------------------------
31
+ // Task result parser
32
+ // ---------------------------------------------------------------------------
33
+
29
34
  /** Extract the last JSON block from agent output text. */
30
35
  export function parseStructuredResult(text: string): Record<string, unknown> | null {
31
36
  // Try fenced code blocks first
@@ -47,50 +52,4 @@ export function parseStructuredResult(text: string): Record<string, unknown> | n
47
52
  }
48
53
 
49
54
  return null;
50
- }
51
-
52
- // ---------------------------------------------------------------------------
53
- // Semver helpers (from context.ts)
54
- // ---------------------------------------------------------------------------
55
-
56
- interface SemverParts {
57
- major: number;
58
- minor: number;
59
- patch: number;
60
- pre: string;
61
- }
62
-
63
- export function parseSemver(v: string): SemverParts {
64
- const cleaned = v.replace(/^v/i, "");
65
- const [main, pre] = cleaned.split("-", 2);
66
- const [major, minor, patch] = (main ?? "0.0.0").split(".").map(Number);
67
- return { major: major || 0, minor: minor || 0, patch: patch || 0, pre: pre ?? "" };
68
- }
69
-
70
- /**
71
- * Lightweight semver "greater than" check. Returns true if a > b.
72
- * Handles prerelease tags (1.4.0 > 1.4.0-beta.1, 1.4.0-beta.5 > 1.4.0-beta.4).
73
- */
74
- export function semverGt(a: string, b: string): boolean {
75
- const va = parseSemver(a);
76
- const vb = parseSemver(b);
77
-
78
- if (va.major !== vb.major) return va.major > vb.major;
79
- if (va.minor !== vb.minor) return va.minor > vb.minor;
80
- if (va.patch !== vb.patch) return va.patch > vb.patch;
81
-
82
- // Release > prerelease
83
- if (!va.pre && vb.pre) return true;
84
- if (va.pre && !vb.pre) return false;
85
-
86
- // Prerelease comparison: split into label + number (e.g. "beta.10" -> ["beta", 10])
87
- const aParts = va.pre.split(".");
88
- const bParts = vb.pre.split(".");
89
- const aLabel = aParts.slice(0, -1).join(".");
90
- const bLabel = bParts.slice(0, -1).join(".");
91
- if (aLabel !== bLabel) return aLabel > bLabel;
92
- const aNum = parseInt(aParts[aParts.length - 1] ?? "0", 10);
93
- const bNum = parseInt(bParts[bParts.length - 1] ?? "0", 10);
94
- if (!isNaN(aNum) && !isNaN(bNum) && aNum !== bNum) return aNum > bNum;
95
- return va.pre > vb.pre;
96
- }
55
+ }
@@ -6,6 +6,15 @@
6
6
  * (depend on build-time placeholder replacement).
7
7
  */
8
8
 
9
+ // ---------------------------------------------------------------------------
10
+ // Error helpers
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /** Extract message from an unknown catch value. */
14
+ export function errMsg(err: unknown): string {
15
+ return err instanceof Error ? err.message : String(err);
16
+ }
17
+
9
18
  // ---------------------------------------------------------------------------
10
19
  // Logger factory
11
20
  // ---------------------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "bereach-openclaw",
3
3
  "name": "BeReach",
4
- "version": "1.5.8",
4
+ "version": "1.5.10",
5
5
  "description": "LinkedIn outreach automation — 75+ tools, hook-based enforcement, dynamic context",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bereach-openclaw",
3
- "version": "1.5.8",
3
+ "version": "1.5.10",
4
4
  "description": "BeReach LinkedIn automation plugin for OpenClaw",
5
5
  "license": "AGPL-3.0",
6
6
  "exports": {
@@ -50,12 +50,12 @@
50
50
  "bereach": "1.5.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^25.5.0",
54
- "@upstash/box": "^0.1.30",
53
+ "@playwright/test": "^1.59.1",
54
+ "@types/node": "^25.5.2",
55
+ "@upstash/box": "^0.1.32",
55
56
  "tsx": "^4.21.0",
56
57
  "typescript": "^6.0.2",
57
- "@playwright/test": "^1.52.0",
58
- "vitest": "^4.1.2",
58
+ "vitest": "^4.1.4",
59
59
  "zod": "^4.3.6"
60
60
  }
61
61
  }
@@ -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: 1775565279
4
+ lastUpdatedAt: 1775759685
5
5
  metadata: { "openclaw": { "requires": { "env": ["BEREACH_API_KEY"] }, "primaryEnv": "BEREACH_API_KEY" } }
6
6
  ---
7
7
 
@@ -28,7 +28,7 @@ Load sub-skills **on-demand** when the user's request matches a workflow.
28
28
  | Warmup | warmup, warm up, account warmup, engagement, likes, visibility, ramp up, pre-warming | sub/warmup.md | 1775410333 |
29
29
  | Content | content, post, publish, LinkedIn post, content strategy, draft, article, thought leadership | sub/content.md | 1775410333 |
30
30
  | Inbox | inbox, triage, classify, archive, star, respond, unread, conversation, spam, inbox management | sub/inbox.md | 1775410333 |
31
- | SDK Reference | sdk, method, parameter, script, TypeScript, generate code, automate | sdk-reference.md | 1775486606 |
31
+ | SDK Reference | sdk, method, parameter, script, TypeScript, generate code, automate | sdk-reference.md | 1775759685 |
32
32
 
33
33
  ### Workspace Templates
34
34
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: bereach-sdk-reference
3
3
  description: "Complete SDK method reference — parameters, types, and descriptions for all BeReach operations."
4
- lastUpdatedAt: 1775486606
4
+ lastUpdatedAt: 1775759685
5
5
  ---
6
6
 
7
7
  <!--
@@ -1374,12 +1374,3 @@ Save a conversation summary after reviewing DM history. Include: topics, relatio
1374
1374
  - **profile** (string, required) — LinkedIn profile URL.
1375
1375
  - **summary** (string, required) — Your conversation summary (max 10,000 chars). Be concise but comprehensive.
1376
1376
 
1377
- ## __self_upgrade__
1378
-
1379
- ### undefined
1380
-
1381
- Upgrade the BeReach plugin to the latest version. Runs npm install and requires a session restart afterward.
1382
-
1383
- `client.__self_upgrade__(params)`
1384
-
1385
- No parameters.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * HTTP helpers for communicating with the BeReach API and gateway.
3
+ */
4
+
5
+ import { type ConnectorConfig, type PullResponse, AuthError, MIN_POLL_MS } from "./types";
6
+ import { errMsg } from "@bereach/tools/utils";
7
+
8
+ export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
9
+ return new Promise((resolve) => {
10
+ const delay = Math.max(MIN_POLL_MS, ms);
11
+ const timer = setTimeout(resolve, delay);
12
+ signal?.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
13
+ });
14
+ }
15
+
16
+ export function buildHeaders(config: ConnectorConfig): Record<string, string> {
17
+ const headers: Record<string, string> = { "Content-Type": "application/json" };
18
+ if (config.apiKey) {
19
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
20
+ }
21
+ return headers;
22
+ }
23
+
24
+ export async function httpPost(url: string, body: unknown, headers: Record<string, string>): Promise<unknown> {
25
+ const res = await fetch(url, {
26
+ method: "POST",
27
+ headers,
28
+ body: JSON.stringify(body),
29
+ });
30
+ if (!res.ok) {
31
+ const text = await res.text().catch(() => "");
32
+ if (res.status === 401 || res.status === 403) {
33
+ throw new AuthError(res.status, text);
34
+ }
35
+ throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
36
+ }
37
+ return res.json();
38
+ }
39
+
40
+ export async function pullTask(config: ConnectorConfig): Promise<PullResponse> {
41
+ const headers = buildHeaders(config);
42
+ const body: Record<string, unknown> = {};
43
+ if (!config.apiKey && config.connectorToken) {
44
+ body.connectorToken = config.connectorToken;
45
+ }
46
+ return httpPost(`${config.apiUrl}/tasks/pull`, body, headers) as Promise<PullResponse>;
47
+ }
48
+
49
+ export async function submitResult(
50
+ config: ConnectorConfig,
51
+ taskId: string,
52
+ result: unknown,
53
+ error: string | null,
54
+ ): Promise<void> {
55
+ const headers = buildHeaders(config);
56
+ const body: Record<string, unknown> = { result, error };
57
+ if (!config.apiKey && config.connectorToken) {
58
+ body.connectorToken = config.connectorToken;
59
+ }
60
+ await httpPost(`${config.apiUrl}/tasks/${taskId}/result`, body, headers);
61
+ }
62
+
63
+ export async function heartbeat(
64
+ config: ConnectorConfig,
65
+ currentTaskId?: string,
66
+ ): Promise<{ pollIntervalMs: number }> {
67
+ const headers = buildHeaders(config);
68
+ const body: Record<string, unknown> = { currentTaskId };
69
+ if (!config.apiKey && config.connectorToken) {
70
+ body.connectorToken = config.connectorToken;
71
+ }
72
+ return httpPost(`${config.apiUrl}/connectors/heartbeat`, body, headers) as Promise<{ pollIntervalMs: number }>;
73
+ }
74
+
75
+ export async function updateTaskStatus(
76
+ config: ConnectorConfig,
77
+ taskId: string,
78
+ status: "accepted" | "running",
79
+ ): Promise<void> {
80
+ const headers = buildHeaders(config);
81
+ const body: Record<string, unknown> = { status };
82
+ if (!config.apiKey && config.connectorToken) {
83
+ body.connectorToken = config.connectorToken;
84
+ }
85
+ await httpPost(`${config.apiUrl}/tasks/${taskId}/result`, body, headers).catch((err) => {
86
+ console.error(`[connector] Task status update to "${status}" failed for ${taskId}: ${errMsg(err)}`);
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Check if gateway webhook is available.
92
+ * Uses a HEAD request to avoid spawning an actual agent session.
93
+ * Falls back to checking if the gateway root responds at all.
94
+ */
95
+ export async function isWebhookAvailable(config: ConnectorConfig): Promise<boolean> {
96
+ if (!config.gatewayUrl || !config.hooksToken) return false;
97
+ try {
98
+ const res = await fetch(`${config.gatewayUrl}/hooks/agent`, {
99
+ method: "OPTIONS",
100
+ headers: {
101
+ "Authorization": `Bearer ${config.hooksToken}`,
102
+ },
103
+ signal: AbortSignal.timeout(5000),
104
+ });
105
+ if (res.status === 401 || res.status === 403) return false;
106
+ if (res.ok || res.status === 405 || res.status === 204) return true;
107
+
108
+ console.log(`[connector] Webhook probe: OPTIONS /hooks/agent returned ${res.status}, hooks not available`);
109
+ return false;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }