bereach-openclaw 1.5.8 → 1.5.9
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/package.json +4 -1
- package/node_modules/@bereach/tools/src/api-client.ts +158 -0
- package/node_modules/@bereach/tools/src/cost-estimation.ts +69 -0
- package/node_modules/@bereach/tools/src/definitions.ts +0 -12
- package/node_modules/@bereach/tools/src/enforcement-types.ts +0 -26
- package/node_modules/@bereach/tools/src/index.ts +16 -4
- package/node_modules/@bereach/tools/src/llm-errors.ts +24 -0
- package/node_modules/@bereach/tools/src/parse-utils.ts +6 -47
- package/node_modules/@bereach/tools/src/utils.ts +9 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -5
- package/skills/bereach/SKILL.md +2 -2
- package/skills/bereach/sdk-reference.md +1 -10
- package/src/commands/connector/api.ts +113 -0
- package/src/commands/connector/execution.ts +126 -0
- package/src/commands/connector/index.ts +380 -0
- package/src/commands/connector/types.ts +79 -0
- package/src/commands/setup.ts +3 -2
- package/src/connector/manager.ts +11 -18
- package/src/connector-cli.ts +1 -1
- package/src/hooks/context/formatters.ts +580 -0
- package/src/hooks/context/index.ts +311 -0
- package/src/hooks/context/task-context.ts +79 -0
- package/src/hooks/enforcement.ts +11 -100
- package/src/hooks/lifecycle.ts +23 -117
- package/src/hooks/tracking.ts +11 -10
- package/src/hooks/types.ts +0 -2
- package/src/hooks/utils.ts +1 -0
- package/src/index.ts +61 -23
- package/src/tools/index.ts +8 -117
- package/src/commands/connector.ts +0 -997
- package/src/hooks/context.ts +0 -1133
- package/src/hooks/enforcement-rules.ts +0 -5
- 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",
|
|
@@ -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,
|
|
10
|
+
createSessionState,
|
|
11
11
|
type PluginConfig, type SessionState, type TaskModeInfo,
|
|
12
|
-
type DbCampaign,
|
|
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 (
|
|
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
|
// ---------------------------------------------------------------------------
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bereach-openclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.9",
|
|
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
|
-
"@
|
|
54
|
-
"@
|
|
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
|
-
"
|
|
58
|
-
"vitest": "^4.1.2",
|
|
58
|
+
"vitest": "^4.1.4",
|
|
59
59
|
"zod": "^4.3.6"
|
|
60
60
|
}
|
|
61
61
|
}
|
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: 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 |
|
|
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:
|
|
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
|
+
}
|