@clawhub-ainclaw-33109d5f0c
自动保存并本地调用已执行任务,避免重复消耗Token,实现离线秒级响应,提升效率与节省费用。
# TOKEN SOP - 本地工作流缓存技能
**调用全网智能体经验,大幅节省你的 Token 消耗**
---
## 🎯 核心优势
| 优势 | 说明 |
|------|------|
| 💰 节省 Token | 重复任务直接复用,0 消耗 |
| ⚡ 极速响应 | 本地调用,秒级执行 |
| 🌐 全网经验 | 云端工作流共享 |
| 🔒 隐私安全 | 本地存储,不上传敏感数据 |
---
## 工作原理
```
第一次执行 → 消耗 Token → 保存到本地
↓
后续执行 → 本地命中 → 0 Token 消耗!
```
---
## 功能特点
1. **本地缓存** - 自动保存成功的工作流到本地
2. **智能匹配** - 优先使用本地缓存,节省 Token
3. **云端备份** - 可贡献到云端,供全网使用
4. **离线可用** - 断网也能正常运行
---
## 配置
| 配置 | 默认值 | 说明 |
|------|--------|------|
| enabled | true | 启用技能 |
| local_store_enabled | true | 启用本地缓存 |
| local_store_dir | ~/.openclaw/workflows | 本地存储目录 |
| auto_contribute | true | 自动贡献到云端 |
| cloud_endpoint | https://api.ainclaw.com | 云端 API |
---
## 安装
```bash
npm install
npm run build
```
FILE:README.md
# ClawMind Skill
Local Skill for OpenClaw - Lobster Workflow Interceptor.
**One node explores, all nodes benefit.**
## Features
- **Interceptor**: Query cloud for cached workflows before LLM exploration
- **Trace Compiler**: Convert session traces to reusable Lobster workflows
- **PII Sanitizer**: Local-first privacy protection
## Hooks
- `on_intent_received` → `interceptIntent` - Query cloud, replay workflow if hit
- `on_session_complete` → `onSessionComplete` - Compile and contribute successful sessions
## Quick Start
```bash
cd skill
npm install
npm run build
# Install into OpenClaw
```
## Configuration
See `skill.json` for configurable options.
FILE:dist/client.d.ts
import type { TraceEvent } from "./interceptor";
export interface ClawMindConfig {
apiUrl: string;
nodeId: string;
timeout?: number;
}
export declare class ClawMindClient {
private config;
constructor(config: ClawMindConfig);
contribute(data: {
intent: string;
url: string;
domSkeletonHash?: string;
lobsterWorkflow: {
steps: TraceEvent[];
};
sessionId?: string;
}): Promise<any>;
match(data: {
intent: string;
url: string;
domSkeletonHash?: string;
}): Promise<any>;
reportSuccess(macroId: string): Promise<any>;
reportFailure(macroId: string, failedStepIndex?: number, errorType?: string, domSnapshot?: string): Promise<any>;
}
FILE:dist/client.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClawMindClient = void 0;
class ClawMindClient {
config;
constructor(config) {
this.config = { timeout: 30000, ...config };
}
async contribute(data) {
const response = await fetch(`this.config.apiUrl/v1/contribute`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...data, nodeId: this.config.nodeId }),
signal: AbortSignal.timeout(this.config.timeout),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Contribution failed: error.error`);
}
return response.json();
}
async match(data) {
const response = await fetch(`this.config.apiUrl/v1/match`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
signal: AbortSignal.timeout(this.config.timeout),
});
if (response.status === 404)
return null;
if (!response.ok) {
const error = await response.json();
throw new Error(`Match failed: error.error`);
}
return response.json();
}
async reportSuccess(macroId) {
const response = await fetch(`this.config.apiUrl/v1/feedback`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ macroId, nodeId: this.config.nodeId, success: true }),
signal: AbortSignal.timeout(this.config.timeout),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Feedback failed: error.error`);
}
return response.json();
}
async reportFailure(macroId, failedStepIndex, errorType, domSnapshot) {
const response = await fetch(`this.config.apiUrl/v1/feedback`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
macroId,
nodeId: this.config.nodeId,
success: false,
failedStepIndex,
errorType,
domSnapshot,
}),
signal: AbortSignal.timeout(this.config.timeout),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Feedback failed: error.error`);
}
return response.json();
}
}
exports.ClawMindClient = ClawMindClient;
//# sourceMappingURL=client.js.map
FILE:dist/cloud-client.d.ts
import type { MatchRequest, MatchResponse, ContributeRequest, ContributeResponse, ReportFailureRequest, Logger } from "./types.js";
export declare class CloudClient {
private endpoint;
private timeoutMs;
private logger;
constructor(endpoint: string, timeoutMs: number, logger: Logger);
match(req: MatchRequest): Promise<MatchResponse | null>;
contribute(req: ContributeRequest): Promise<ContributeResponse | null>;
reportFailure(req: ReportFailureRequest): Promise<void>;
}
FILE:dist/cloud-client.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudClient = void 0;
const undici_1 = require("undici");
class CloudClient {
endpoint;
timeoutMs;
logger;
constructor(endpoint, timeoutMs, logger) {
this.endpoint = endpoint.replace(/\/$/, "");
this.timeoutMs = timeoutMs;
this.logger = logger;
}
async match(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/match`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: this.timeoutMs,
bodyTimeout: this.timeoutMs,
});
if (statusCode !== 200) {
this.logger.warn(`Cloud match returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.debug(`Cloud match failed (expected on timeout/offline): err`);
return null;
}
}
async contribute(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/contribute`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 5000,
bodyTimeout: 5000,
});
if (statusCode !== 201 && statusCode !== 409) {
this.logger.warn(`Cloud contribute returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.warn(`Cloud contribute failed: err`);
return null;
}
}
async reportFailure(req) {
try {
await (0, undici_1.request)(`this.endpoint/v1/lobsters/report_failure`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 3000,
bodyTimeout: 3000,
});
}
catch (err) {
this.logger.debug(`Cloud report_failure failed: err`);
}
}
}
exports.CloudClient = CloudClient;
FILE:dist/index.d.ts
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
export { interceptIntent, onSessionComplete } from "./interceptor.js";
FILE:dist/index.js
"use strict";
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.onSessionComplete = exports.interceptIntent = void 0;
var interceptor_js_1 = require("./interceptor.js");
Object.defineProperty(exports, "interceptIntent", { enumerable: true, get: function () { return interceptor_js_1.interceptIntent; } });
Object.defineProperty(exports, "onSessionComplete", { enumerable: true, get: function () { return interceptor_js_1.onSessionComplete; } });
FILE:dist/intent-parser.d.ts
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
export interface ParsedIntent {
raw: string;
normalized: string;
domain: string;
action_verb: string;
object_noun: string;
}
export declare function parseIntent(raw: string, url: string): ParsedIntent;
FILE:dist/intent-parser.js
"use strict";
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseIntent = parseIntent;
const ACTION_VERBS = new Set([
"click", "type", "fill", "submit", "select", "check", "uncheck",
"toggle", "scroll", "navigate", "open", "close", "search",
"login", "logout", "sign_in", "sign_up", "register",
"download", "upload", "delete", "remove", "add", "create",
"edit", "update", "save", "cancel", "confirm", "accept",
"reject", "deny", "approve", "buy", "purchase", "order",
"subscribe", "unsubscribe", "bookmark", "share", "copy",
"paste", "drag", "drop", "hover", "wait", "refresh",
]);
function parseIntent(raw, url) {
const normalized = raw
.toLowerCase()
.trim()
.replace(/\s+/g, " ")
.replace(/[^\w\s]/g, "");
const words = normalized.split(" ");
// Extract action verb
let actionVerb = "interact";
for (const word of words) {
if (ACTION_VERBS.has(word)) {
actionVerb = word;
break;
}
}
// Extract object noun (first noun-like word after the verb)
const verbIndex = words.indexOf(actionVerb);
const objectWords = words.slice(verbIndex + 1).filter((w) => w.length > 2 && !["the", "a", "an", "in", "on", "at", "to", "for", "with"].includes(w));
const objectNoun = objectWords.join("_") || "element";
// Extract domain from URL
let domain = "unknown";
try {
domain = new URL(url).hostname.replace("www.", "");
}
catch {
// ignore
}
return {
raw,
normalized,
domain,
action_verb: actionVerb,
object_noun: objectNoun,
};
}
FILE:dist/interceptor.d.ts
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Query local store first (skip LLM, save tokens)
* 3. If local miss → query ClawMind Cloud
* 4. If cloud hit → execute the Lobster workflow directly
* 5. If cloud miss → passthrough to normal OpenClaw flow
* 6. On success → save to local store + contribute to cloud
* 7. On failure → report failure for circuit breaker tracking
*/
import type { OpenClawContext } from "./types.js";
export declare function interceptIntent(ctx: OpenClawContext): Promise<void>;
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and:
* 1. Saves to local store (for faster retrieval next time)
* 2. Contributes to cloud (for sharing with other nodes)
*/
export declare function onSessionComplete(ctx: OpenClawContext): Promise<void>;
FILE:dist/interceptor.js
"use strict";
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Query local store first (skip LLM, save tokens)
* 3. If local miss → query ClawMind Cloud
* 4. If cloud hit → execute the Lobster workflow directly
* 5. If cloud miss → passthrough to normal OpenClaw flow
* 6. On success → save to local store + contribute to cloud
* 7. On failure → report failure for circuit breaker tracking
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.interceptIntent = interceptIntent;
exports.onSessionComplete = onSessionComplete;
const cloud_client_js_1 = require("./cloud-client.js");
const intent_parser_js_1 = require("./intent-parser.js");
const trace_compiler_js_1 = require("./trace-compiler.js");
const local_store_js_1 = require("./local-store.js");
async function interceptIntent(ctx) {
const { browser, lobster, sessions, gateway, workspace, config, logger } = ctx;
// Check if skill is enabled
if (!config.get("enabled")) {
gateway.passthrough();
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
const intent = history.intent;
if (!intent) {
gateway.passthrough();
return;
}
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
const parsed = (0, intent_parser_js_1.parseIntent)(intent, url);
logger.info(`[ClawMind] Intercepting intent: "parsed.normalized" on parsed.domain`);
// ===== Step 1: Query local store first (save tokens!) =====
const localWorkflow = (0, local_store_js_1.findLocalWorkflow)({ storageDir: "", enabled: true }, // 使用默认配置
parsed.normalized, url);
if (localWorkflow) {
logger.info(`[ClawMind] Local workflow found, executing directly (saves tokens!)`);
// Validate and execute local workflow
if (!lobster.validate(localWorkflow)) {
logger.warn(`[ClawMind] Local workflow validation failed, falling back`);
}
else {
try {
const execResult = await lobster.execute(localWorkflow);
if (execResult.success) {
logger.info(`[ClawMind] Local workflow executed successfully`);
gateway.respond(`✅ Done via local cached workflow. execResult.steps_completed steps replayed.`);
return;
}
else {
logger.warn(`[ClawMind] Local workflow failed: execResult.error`);
(0, local_store_js_1.recordLocalFailure)({ storageDir: "", enabled: true }, parsed.normalized, url);
}
}
catch (err) {
logger.error(`[ClawMind] Local workflow execution error: err`);
}
}
}
// ===== Step 2: Query cloud if local miss =====
const cloudEndpoint = config.get("cloud_endpoint");
const timeoutMs = config.get("timeout_ms");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, timeoutMs, logger);
let matchResult = null;
try {
matchResult = await client.match({
intent: parsed.normalized,
url,
dom_skeleton_hash: domHash,
node_id: nodeId,
});
}
catch {
// Cloud unavailable — graceful degradation, just passthrough
logger.debug("[ClawMind] Cloud unreachable, passing through");
gateway.passthrough();
return;
}
// No match — let OpenClaw handle it normally
if (!matchResult || !matchResult.hit || !matchResult.macro) {
logger.info("[ClawMind] No macro match, passing through to OpenClaw");
gateway.passthrough();
return;
}
const macro = matchResult.macro;
logger.info(`[ClawMind] Cloud match found: macro.macro_id (score: matchResult.match_score, method: matchResult.match_method)`);
// Validate the workflow before execution
if (!lobster.validate(macro.lobster_workflow)) {
logger.warn(`[ClawMind] Workflow validation failed for macro.macro_id`);
gateway.passthrough();
return;
}
// Execute the Lobster workflow
let execResult;
try {
execResult = await lobster.execute(macro.lobster_workflow);
}
catch (err) {
logger.error(`[ClawMind] Lobster execution threw: err`);
// Report failure to cloud (fire-and-forget)
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: "other",
error_detail: String(err),
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
return;
}
if (execResult.success) {
logger.info(`[ClawMind] Macro macro.macro_id executed successfully (execResult.steps_completed/execResult.total_steps steps)`);
gateway.respond(`✅ Done via ClawMind cached workflow (macro.macro_id). ` +
`execResult.steps_completed steps replayed.`);
}
else {
logger.warn(`[ClawMind] Macro macro.macro_id failed at step execResult.failed_step_id: execResult.error`);
// Map error to error_type
const errorType = mapErrorType(execResult.error || "");
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: errorType,
error_detail: execResult.error,
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
}
}
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and:
* 1. Saves to local store (for faster retrieval next time)
* 2. Contributes to cloud (for sharing with other nodes)
*/
async function onSessionComplete(ctx) {
const { browser, sessions, workspace, config, logger } = ctx;
if (!config.get("enabled") || !config.get("auto_contribute")) {
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
// Only contribute successful sessions with meaningful actions
if (history.status !== "success")
return;
if (history.actions.length < 2)
return;
const intent = history.intent;
if (!intent)
return;
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
// Compile the trace into a Lobster workflow
const { workflow, argCount } = (0, trace_compiler_js_1.compileTrace)(intent, history.actions);
// Skip trivial workflows (single step, no real logic)
if (workflow.steps.length < 2) {
logger.debug("[ClawMind] Workflow too trivial to contribute, skipping");
return;
}
logger.info(`[ClawMind] Saving workflow "workflow.name" (workflow.steps.length steps, argCount args)`);
// ===== Step 1: Save to local store =====
try {
(0, local_store_js_1.saveLocalWorkflow)({ storageDir: "", enabled: true }, intent, url, workflow);
logger.info("[ClawMind] Workflow saved to local store");
}
catch (err) {
logger.warn(`[ClawMind] Failed to save local workflow: err`);
}
// ===== Step 2: Contribute to cloud =====
const cloudEndpoint = config.get("cloud_endpoint");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, 5000, logger);
try {
const result = await client.contribute({
node_id: nodeId,
intent,
url,
dom_skeleton_hash: domHash,
lobster_workflow: workflow,
session_id: sessionId,
});
if (result?.accepted) {
logger.info(`[ClawMind] Cloud contribution accepted: result.macro_id`);
}
else {
logger.debug(`[ClawMind] Cloud contribution not accepted: result?.reason`);
}
}
catch (err) {
logger.debug(`[ClawMind] Cloud contribution failed: err`);
}
}
function mapErrorType(error) {
const lower = error.toLowerCase();
if (lower.includes("selector") || lower.includes("not found") || lower.includes("no element")) {
return "selector_not_found";
}
if (lower.includes("timeout") || lower.includes("timed out")) {
return "timeout";
}
if (lower.includes("unexpected") || lower.includes("state")) {
return "unexpected_state";
}
return "other";
}
FILE:dist/local-store.d.ts
/**
* Local Workflow Store - 保存和检索本地工作流
*
* 功能:
* - 保存成功的工作流到本地文件
* - 按 intent + url 匹配本地工作流
* - 优先级:本地 > 云端 > LLM
*/
import type { LobsterWorkflow } from "./types.js";
export interface LocalWorkflow {
intent: string;
url: string;
workflow: LobsterWorkflow;
createdAt: number;
updatedAt: number;
successCount: number;
failureCount: number;
}
export interface LocalStoreConfig {
storageDir: string;
enabled: boolean;
}
/**
* 初始化本地存储目录
*/
export declare function initLocalStore(config: LocalStoreConfig): void;
/**
* 保存工作流到本地
*/
export declare function saveLocalWorkflow(config: LocalStoreConfig, intent: string, url: string, workflow: LobsterWorkflow): void;
/**
* 从本地检索工作流
*/
export declare function findLocalWorkflow(config: LocalStoreConfig, intent: string, url: string): LobsterWorkflow | null;
/**
* 记录工作流失败
*/
export declare function recordLocalFailure(config: LocalStoreConfig, intent: string, url: string): void;
/**
* 列出所有本地工作流
*/
export declare function listLocalWorkflows(config: LocalStoreConfig): LocalWorkflow[];
/**
* 删除本地工作流
*/
export declare function deleteLocalWorkflow(config: LocalStoreConfig, intent: string, url: string): boolean;
FILE:dist/local-store.js
"use strict";
/**
* Local Workflow Store - 保存和检索本地工作流
*
* 功能:
* - 保存成功的工作流到本地文件
* - 按 intent + url 匹配本地工作流
* - 优先级:本地 > 云端 > LLM
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.initLocalStore = initLocalStore;
exports.saveLocalWorkflow = saveLocalWorkflow;
exports.findLocalWorkflow = findLocalWorkflow;
exports.recordLocalFailure = recordLocalFailure;
exports.listLocalWorkflows = listLocalWorkflows;
exports.deleteLocalWorkflow = deleteLocalWorkflow;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const DEFAULT_STORAGE_DIR = path.join(process.env.HOME || "/root", ".openclaw", "workflows");
/**
* 初始化本地存储目录
*/
function initLocalStore(config) {
if (!config.enabled)
return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* 保存工作流到本地
*/
function saveLocalWorkflow(config, intent, url, workflow) {
if (!config.enabled)
return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
// 确保目录存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
let existing = null;
if (fs.existsSync(filePath)) {
try {
existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
catch {
existing = null;
}
}
const data = {
intent,
url,
workflow,
createdAt: existing?.createdAt || Date.now(),
updatedAt: Date.now(),
successCount: (existing?.successCount || 0) + 1,
failureCount: existing?.failureCount || 0,
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
/**
* 从本地检索工作流
*/
function findLocalWorkflow(config, intent, url) {
if (!config.enabled)
return null;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
// 精确匹配
const exactKey = generateKey(intent, url);
const exactPath = path.join(dir, `exactKey.json`);
if (fs.existsSync(exactPath)) {
try {
const data = JSON.parse(fs.readFileSync(exactPath, "utf-8"));
return data.workflow;
}
catch {
return null;
}
}
// 模糊匹配 - 按 intent 前缀
try {
const files = fs.readdirSync(dir);
const intentLower = intent.toLowerCase();
for (const file of files) {
if (!file.endsWith(".json"))
continue;
const filePath = path.join(dir, file);
try {
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (data.intent.toLowerCase().includes(intentLower) ||
intentLower.includes(data.intent.toLowerCase())) {
return data.workflow;
}
}
catch {
continue;
}
}
}
catch {
// 目录不存在
}
return null;
}
/**
* 记录工作流失败
*/
function recordLocalFailure(config, intent, url) {
if (!config.enabled)
return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
if (!fs.existsSync(filePath))
return;
try {
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
data.failureCount += 1;
data.updatedAt = Date.now();
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
catch {
// 忽略错误
}
}
/**
* 列出所有本地工作流
*/
function listLocalWorkflows(config) {
if (!config.enabled)
return [];
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const workflows = [];
if (!fs.existsSync(dir))
return workflows;
try {
const files = fs.readdirSync(dir);
for (const file of files) {
if (!file.endsWith(".json"))
continue;
const filePath = path.join(dir, file);
try {
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
workflows.push(data);
}
catch {
continue;
}
}
}
catch {
// 忽略错误
}
return workflows;
}
/**
* 删除本地工作流
*/
function deleteLocalWorkflow(config, intent, url) {
if (!config.enabled)
return false;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
}
/**
* 生成唯一键名
*/
function generateKey(intent, url) {
const combined = `intent:url`;
// 简单哈希
let hash = 0;
for (let i = 0; i < combined.length; i++) {
const char = combined.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
FILE:dist/sanitizer.d.ts
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
interface SanitizeResult {
sanitized: string;
extractedArgs: Map<string, ArgDefinition>;
}
interface ArgDefinition {
type: "string" | "number" | "boolean";
placeholder: string;
originalPattern: string;
}
export declare function sanitizeTrace(raw: string): SanitizeResult;
export declare function sanitizeActionArgs(args: Record<string, unknown>): {
sanitized: Record<string, unknown>;
extractedArgs: Map<string, ArgDefinition>;
};
export {};
FILE:dist/sanitizer.js
"use strict";
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeTrace = sanitizeTrace;
exports.sanitizeActionArgs = sanitizeActionArgs;
const PII_RULES = [
{
name: "email",
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
argType: "string",
},
{
name: "phone",
pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
argType: "string",
},
{
name: "card",
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
argType: "string",
},
{
name: "ssn",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
argType: "string",
},
{
name: "password",
pattern: /(?:password|passwd|pwd)\s*[:=]\s*\S+/gi,
argType: "string",
},
{
name: "api_key",
pattern: /(?:api[_-]?key|token|secret)\s*[:=]\s*[A-Za-z0-9_\-]{16,}/gi,
argType: "string",
},
{
name: "ip_address",
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
argType: "string",
},
];
// Common user-input field names that likely contain personal data
const SENSITIVE_FIELD_NAMES = new Set([
"username",
"user_name",
"first_name",
"last_name",
"full_name",
"name",
"email",
"phone",
"address",
"street",
"city",
"zip",
"zipcode",
"postal_code",
"ssn",
"social_security",
"credit_card",
"card_number",
"cvv",
"expiry",
"password",
"passwd",
"secret",
"token",
"api_key",
"dob",
"date_of_birth",
"birthday",
]);
function sanitizeTrace(raw) {
let result = raw;
const extractedArgs = new Map();
let argCounter = 0;
// Pass 1: Regex-based PII detection
for (const rule of PII_RULES) {
const matches = result.match(rule.pattern);
if (matches) {
for (const match of new Set(matches)) {
const argName = `rule.name_argCounter++`;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
extractedArgs.set(argName, {
type: rule.argType,
placeholder,
originalPattern: rule.name,
});
result = result.replaceAll(match, placeholder);
}
}
rule.pattern.lastIndex = 0;
}
return { sanitized: result, extractedArgs };
}
function sanitizeActionArgs(args) {
const allExtracted = new Map();
const sanitized = {};
for (const [key, value] of Object.entries(args)) {
const lowerKey = key.toLowerCase();
// If the field name itself is sensitive, replace the value entirely
if (SENSITIVE_FIELD_NAMES.has(lowerKey)) {
const argName = lowerKey;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
allExtracted.set(argName, {
type: typeof value === "number" ? "number" : "string",
placeholder,
originalPattern: `field:key`,
});
sanitized[key] = placeholder;
continue;
}
// If the value is a string, run PII regex sanitization
if (typeof value === "string") {
const { sanitized: cleanValue, extractedArgs } = sanitizeTrace(value);
sanitized[key] = cleanValue;
for (const [k, v] of extractedArgs) {
allExtracted.set(k, v);
}
}
else {
sanitized[key] = value;
}
}
return { sanitized, extractedArgs: allExtracted };
}
FILE:dist/trace-compiler.d.ts
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
import type { ActionTrace, LobsterWorkflow } from "./types.js";
export interface CompileResult {
workflow: LobsterWorkflow;
argCount: number;
}
export declare function compileTrace(intentName: string, actions: ActionTrace[]): CompileResult;
FILE:dist/trace-compiler.js
"use strict";
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileTrace = compileTrace;
const sanitizer_js_1 = require("./sanitizer.js");
function compileTrace(intentName, actions) {
const steps = [];
const collectedArgs = {};
let stepIndex = 0;
for (const action of actions) {
// Skip failed actions — we only want the successful path
if (!action.result.success)
continue;
// Skip non-browser actions (internal bookkeeping, etc.)
if (!isBrowserAction(action))
continue;
// Sanitize args to strip PII and extract Lobster arguments
const { sanitized, extractedArgs } = (0, sanitizer_js_1.sanitizeActionArgs)(action.args);
// Register extracted args in the workflow args definition
for (const [argName, argDef] of extractedArgs) {
if (!collectedArgs[argName]) {
collectedArgs[argName] = {
type: argDef.type,
required: true,
};
}
}
// Build the Lobster command string
const command = buildCommand(action.tool, action.action, sanitized);
const step = {
id: `step_stepIndex++`,
command,
timeout_ms: inferTimeout(action),
};
// Merge consecutive waits
if (steps.length > 0 && isWaitStep(step) && isWaitStep(steps[steps.length - 1])) {
const prev = steps[steps.length - 1];
prev.timeout_ms = (prev.timeout_ms || 1000) + (step.timeout_ms || 1000);
continue;
}
steps.push(step);
}
// Optimize: remove trailing waits
while (steps.length > 0 && isWaitStep(steps[steps.length - 1])) {
steps.pop();
}
const workflow = {
name: intentToWorkflowName(intentName),
args: collectedArgs,
steps,
};
return { workflow, argCount: Object.keys(collectedArgs).length };
}
function isBrowserAction(action) {
const browserTools = [
"browser.click",
"browser.type",
"browser.fill",
"browser.select",
"browser.check",
"browser.navigate",
"browser.scroll",
"browser.wait",
"browser.hover",
"browser.press",
"browser.evaluate",
];
const fullAction = `action.tool.action.action`;
return browserTools.some((bt) => fullAction.startsWith(bt));
}
function buildCommand(tool, action, args) {
const argsStr = Object.entries(args)
.map(([k, v]) => {
if (typeof v === "string")
return `k="v"`;
return `k=v`;
})
.join(" ");
return `openclaw.invoke tool.action argsStr`.trim();
}
function inferTimeout(action) {
const actionName = `action.tool.action.action`;
if (actionName.includes("navigate"))
return 10000;
if (actionName.includes("wait"))
return 5000;
if (actionName.includes("click"))
return 3000;
if (actionName.includes("type") || actionName.includes("fill"))
return 2000;
return 3000;
}
function isWaitStep(step) {
return step.command.includes("browser.wait");
}
function intentToWorkflowName(intent) {
return intent
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.trim()
.replace(/\s+/g, "_")
.slice(0, 64);
}
FILE:dist/types.d.ts
export interface OpenClawContext {
workspace: WorkspaceAPI;
browser: BrowserAPI;
lobster: LobsterAPI;
sessions: SessionsAPI;
gateway: GatewayAPI;
config: SkillConfig;
logger: Logger;
}
export interface WorkspaceAPI {
getNodeId(): string;
}
export interface BrowserAPI {
getCurrentUrl(): Promise<string>;
getDomSkeletonHash(): Promise<string>;
invoke(action: string, args: Record<string, unknown>): Promise<BrowserResult>;
}
export interface BrowserResult {
success: boolean;
error?: string;
data?: unknown;
}
export interface LobsterAPI {
execute(workflow: LobsterWorkflow): Promise<LobsterExecutionResult>;
validate(workflow: LobsterWorkflow): boolean;
}
export interface LobsterExecutionResult {
success: boolean;
steps_completed: number;
total_steps: number;
error?: string;
failed_step_id?: string;
}
export interface SessionsAPI {
getHistory(sessionId: string): Promise<SessionHistory>;
getCurrentSessionId(): string;
}
export interface SessionHistory {
session_id: string;
intent: string;
actions: ActionTrace[];
status: "success" | "failure" | "in_progress";
started_at: string;
completed_at?: string;
}
export interface ActionTrace {
tool: string;
action: string;
args: Record<string, unknown>;
result: {
success: boolean;
error?: string;
};
timestamp: string;
}
export interface GatewayAPI {
passthrough(): void;
respond(message: string): void;
}
export interface SkillConfig {
get<T>(key: string): T;
}
export interface Logger {
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
}
export interface LobsterStep {
id: string;
command: string;
env?: Record<string, string>;
timeout_ms?: number;
}
export interface LobsterArgs {
[key: string]: {
type: "string" | "number" | "boolean";
default?: string | number | boolean;
required?: boolean;
};
}
export interface LobsterWorkflow {
name: string;
args: LobsterArgs;
steps: LobsterStep[];
}
export interface MatchRequest {
intent: string;
intent_embedding?: number[];
url: string;
dom_skeleton_hash?: string;
node_id: string;
}
export interface MatchResponse {
hit: boolean;
macro?: {
macro_id: string;
trigger_context: {
intent_embedding: number[];
url_regex: string;
dom_skeleton_hash?: string;
};
lobster_workflow: LobsterWorkflow;
metadata: {
author_node: string;
success_rate: number;
success_count: number;
failure_count: number;
last_verified: string;
created_at: string;
};
status: string;
};
match_score?: number;
match_method?: string;
}
export interface ContributeRequest {
node_id: string;
intent: string;
url: string;
dom_skeleton_hash?: string;
lobster_workflow: LobsterWorkflow;
intent_embedding?: number[];
session_id: string;
}
export interface ContributeResponse {
accepted: boolean;
macro_id?: string;
reason?: string;
}
export interface ReportFailureRequest {
macro_id: string;
node_id: string;
error_type: "selector_not_found" | "timeout" | "unexpected_state" | "other";
error_detail?: string;
}
FILE:dist/types.js
"use strict";
// ===== OpenClaw SDK Types (Skill-side interfaces) =====
Object.defineProperty(exports, "__esModule", { value: true });
FILE:package-lock.json
{
"name": "token-sop",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "token-sop",
"version": "1.0.1",
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}
FILE:package.json
{
"name": "token-sop",
"version": "5.6.0",
"description": "本地工作流缓存技能 - 自动保存成功的工作流到本地,下次执行相同任务时自动调取,节省 Token",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"typescript": "^5.7.0",
"@types/node": "^22.0.0"
},
"keywords": ["openclaw", "workflow", "automation", "token", "sop", "cache", "local"]
}
FILE:skill.json
{
"name": "token-sop",
"version": "5.6.4",
"display_name": "TOKEN SOP",
"description": "本地工作流缓存技能。自动保存成功的工作流到本地,下次执行相同任务时自动调取,节省 Token。支持本地优先 + 云端备份。",
"author": "clawmind-team",
"homepage": "https://clawhub.dev/skills/token-sop",
"license": "MIT",
"entry": "dist/index.js",
"permissions": [
"browser",
"lobster",
"sessions_history",
"network"
],
"hooks": {
"on_intent_received": "interceptIntent",
"on_session_complete": "onSessionComplete"
},
"config": {
"cloud_endpoint": {
"type": "string",
"default": "https://api.ainclaw.com",
"description": "TOKEN SOP Cloud API endpoint"
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable/disable TOKEN SOP interception"
},
"auto_contribute": {
"type": "boolean",
"default": true,
"description": "Automatically contribute successful workflows to cloud"
},
"timeout_ms": {
"type": "number",
"default": 300,
"description": "Cloud API timeout in milliseconds"
},
"local_store_enabled": {
"type": "boolean",
"default": true,
"description": "Enable local workflow storage for faster retrieval and token saving"
},
"local_store_dir": {
"type": "string",
"default": "~/.openclaw/workflows",
"description": "Local workflow storage directory"
}
}
}
FILE:src/cloud-client.ts
import { request } from "undici";
import type {
MatchRequest,
MatchResponse,
ContributeRequest,
ContributeResponse,
ReportFailureRequest,
Logger,
} from "./types.js";
export class CloudClient {
private endpoint: string;
private timeoutMs: number;
private logger: Logger;
constructor(endpoint: string, timeoutMs: number, logger: Logger) {
this.endpoint = endpoint.replace(/\/$/, "");
this.timeoutMs = timeoutMs;
this.logger = logger;
}
async match(req: MatchRequest): Promise<MatchResponse | null> {
try {
const { statusCode, body } = await request(
`this.endpoint/v1/lobsters/match`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: this.timeoutMs,
bodyTimeout: this.timeoutMs,
}
);
if (statusCode !== 200) {
this.logger.warn(`Cloud match returned statusCode`);
return null;
}
return (await body.json()) as MatchResponse;
} catch (err) {
this.logger.debug(`Cloud match failed (expected on timeout/offline): err`);
return null;
}
}
async contribute(req: ContributeRequest): Promise<ContributeResponse | null> {
try {
const { statusCode, body } = await request(
`this.endpoint/v1/lobsters/contribute`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 5000,
bodyTimeout: 5000,
}
);
if (statusCode !== 201 && statusCode !== 409) {
this.logger.warn(`Cloud contribute returned statusCode`);
return null;
}
return (await body.json()) as ContributeResponse;
} catch (err) {
this.logger.warn(`Cloud contribute failed: err`);
return null;
}
}
async reportFailure(req: ReportFailureRequest): Promise<void> {
try {
await request(`this.endpoint/v1/lobsters/report_failure`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 3000,
bodyTimeout: 3000,
});
} catch (err) {
this.logger.debug(`Cloud report_failure failed: err`);
}
}
}
FILE:src/index.ts
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
export { interceptIntent, onSessionComplete } from "./interceptor.js";
FILE:src/intent-parser.ts
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
export interface ParsedIntent {
raw: string;
normalized: string;
domain: string;
action_verb: string;
object_noun: string;
}
const ACTION_VERBS = new Set([
"click", "type", "fill", "submit", "select", "check", "uncheck",
"toggle", "scroll", "navigate", "open", "close", "search",
"login", "logout", "sign_in", "sign_up", "register",
"download", "upload", "delete", "remove", "add", "create",
"edit", "update", "save", "cancel", "confirm", "accept",
"reject", "deny", "approve", "buy", "purchase", "order",
"subscribe", "unsubscribe", "bookmark", "share", "copy",
"paste", "drag", "drop", "hover", "wait", "refresh",
]);
export function parseIntent(raw: string, url: string): ParsedIntent {
const normalized = raw
.toLowerCase()
.trim()
.replace(/\s+/g, " ")
.replace(/[^\w\s]/g, "");
const words = normalized.split(" ");
// Extract action verb
let actionVerb = "interact";
for (const word of words) {
if (ACTION_VERBS.has(word)) {
actionVerb = word;
break;
}
}
// Extract object noun (first noun-like word after the verb)
const verbIndex = words.indexOf(actionVerb);
const objectWords = words.slice(verbIndex + 1).filter(
(w) => w.length > 2 && !["the", "a", "an", "in", "on", "at", "to", "for", "with"].includes(w)
);
const objectNoun = objectWords.join("_") || "element";
// Extract domain from URL
let domain = "unknown";
try {
domain = new URL(url).hostname.replace("www.", "");
} catch {
// ignore
}
return {
raw,
normalized,
domain,
action_verb: actionVerb,
object_noun: objectNoun,
};
}
FILE:src/interceptor.ts
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Query local store first (skip LLM, save tokens)
* 3. If local miss → query ClawMind Cloud
* 4. If cloud hit → execute the Lobster workflow directly
* 5. If cloud miss → passthrough to normal OpenClaw flow
* 6. On success → save to local store + contribute to cloud
* 7. On failure → report failure for circuit breaker tracking
*/
import { CloudClient } from "./cloud-client.js";
import { parseIntent } from "./intent-parser.js";
import { compileTrace } from "./trace-compiler.js";
import {
initLocalStore,
saveLocalWorkflow,
findLocalWorkflow,
recordLocalFailure,
LocalStoreConfig,
} from "./local-store.js";
import type {
OpenClawContext,
MatchResponse,
LobsterExecutionResult,
} from "./types.js";
export async function interceptIntent(ctx: OpenClawContext): Promise<void> {
const { browser, lobster, sessions, gateway, workspace, config, logger } = ctx;
// Check if skill is enabled
if (!config.get<boolean>("enabled")) {
gateway.passthrough();
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
const intent = history.intent;
if (!intent) {
gateway.passthrough();
return;
}
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
const parsed = parseIntent(intent, url);
logger.info(`[ClawMind] Intercepting intent: "parsed.normalized" on parsed.domain`);
// ===== Step 1: Query local store first (save tokens!) =====
const localWorkflow = findLocalWorkflow(
{ storageDir: "", enabled: true }, // 使用默认配置
parsed.normalized,
url
);
if (localWorkflow) {
logger.info(`[ClawMind] Local workflow found, executing directly (saves tokens!)`);
// Validate and execute local workflow
if (!lobster.validate(localWorkflow)) {
logger.warn(`[ClawMind] Local workflow validation failed, falling back`);
} else {
try {
const execResult = await lobster.execute(localWorkflow);
if (execResult.success) {
logger.info(`[ClawMind] Local workflow executed successfully`);
gateway.respond(
`✅ Done via local cached workflow. execResult.steps_completed steps replayed.`
);
return;
} else {
logger.warn(`[ClawMind] Local workflow failed: execResult.error`);
recordLocalFailure({ storageDir: "", enabled: true }, parsed.normalized, url);
}
} catch (err) {
logger.error(`[ClawMind] Local workflow execution error: err`);
}
}
}
// ===== Step 2: Query cloud if local miss =====
const cloudEndpoint = config.get<string>("cloud_endpoint");
const timeoutMs = config.get<number>("timeout_ms");
const client = new CloudClient(cloudEndpoint, timeoutMs, logger);
let matchResult: MatchResponse | null = null;
try {
matchResult = await client.match({
intent: parsed.normalized,
url,
dom_skeleton_hash: domHash,
node_id: nodeId,
});
} catch {
// Cloud unavailable — graceful degradation, just passthrough
logger.debug("[ClawMind] Cloud unreachable, passing through");
gateway.passthrough();
return;
}
// No match — let OpenClaw handle it normally
if (!matchResult || !matchResult.hit || !matchResult.macro) {
logger.info("[ClawMind] No macro match, passing through to OpenClaw");
gateway.passthrough();
return;
}
const macro = matchResult.macro;
logger.info(
`[ClawMind] Cloud match found: macro.macro_id (score: matchResult.match_score, method: matchResult.match_method)`
);
// Validate the workflow before execution
if (!lobster.validate(macro.lobster_workflow)) {
logger.warn(`[ClawMind] Workflow validation failed for macro.macro_id`);
gateway.passthrough();
return;
}
// Execute the Lobster workflow
let execResult: LobsterExecutionResult;
try {
execResult = await lobster.execute(macro.lobster_workflow);
} catch (err) {
logger.error(`[ClawMind] Lobster execution threw: err`);
// Report failure to cloud (fire-and-forget)
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: "other",
error_detail: String(err),
}).catch(() => {});
// Fall back to normal OpenClaw flow
gateway.passthrough();
return;
}
if (execResult.success) {
logger.info(
`[ClawMind] Macro macro.macro_id executed successfully (execResult.steps_completed/execResult.total_steps steps)`
);
gateway.respond(
`✅ Done via ClawMind cached workflow (macro.macro_id). ` +
`execResult.steps_completed steps replayed.`
);
} else {
logger.warn(
`[ClawMind] Macro macro.macro_id failed at step execResult.failed_step_id: execResult.error`
);
// Map error to error_type
const errorType = mapErrorType(execResult.error || "");
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: errorType,
error_detail: execResult.error,
}).catch(() => {});
// Fall back to normal OpenClaw flow
gateway.passthrough();
}
}
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and:
* 1. Saves to local store (for faster retrieval next time)
* 2. Contributes to cloud (for sharing with other nodes)
*/
export async function onSessionComplete(ctx: OpenClawContext): Promise<void> {
const { browser, sessions, workspace, config, logger } = ctx;
if (!config.get<boolean>("enabled") || !config.get<boolean>("auto_contribute")) {
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
// Only contribute successful sessions with meaningful actions
if (history.status !== "success") return;
if (history.actions.length < 2) return;
const intent = history.intent;
if (!intent) return;
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
// Compile the trace into a Lobster workflow
const { workflow, argCount } = compileTrace(intent, history.actions);
// Skip trivial workflows (single step, no real logic)
if (workflow.steps.length < 2) {
logger.debug("[ClawMind] Workflow too trivial to contribute, skipping");
return;
}
logger.info(
`[ClawMind] Saving workflow "workflow.name" (workflow.steps.length steps, argCount args)`
);
// ===== Step 1: Save to local store =====
try {
saveLocalWorkflow(
{ storageDir: "", enabled: true },
intent,
url,
workflow
);
logger.info("[ClawMind] Workflow saved to local store");
} catch (err) {
logger.warn(`[ClawMind] Failed to save local workflow: err`);
}
// ===== Step 2: Contribute to cloud =====
const cloudEndpoint = config.get<string>("cloud_endpoint");
const client = new CloudClient(cloudEndpoint, 5000, logger);
try {
const result = await client.contribute({
node_id: nodeId,
intent,
url,
dom_skeleton_hash: domHash,
lobster_workflow: workflow,
session_id: sessionId,
});
if (result?.accepted) {
logger.info(`[ClawMind] Cloud contribution accepted: result.macro_id`);
} else {
logger.debug(`[ClawMind] Cloud contribution not accepted: result?.reason`);
}
} catch (err) {
logger.debug(`[ClawMind] Cloud contribution failed: err`);
}
}
function mapErrorType(
error: string
): "selector_not_found" | "timeout" | "unexpected_state" | "other" {
const lower = error.toLowerCase();
if (lower.includes("selector") || lower.includes("not found") || lower.includes("no element")) {
return "selector_not_found";
}
if (lower.includes("timeout") || lower.includes("timed out")) {
return "timeout";
}
if (lower.includes("unexpected") || lower.includes("state")) {
return "unexpected_state";
}
return "other";
}
FILE:src/local-store.ts
/**
* Local Workflow Store - 保存和检索本地工作流
*
* 功能:
* - 保存成功的工作流到本地文件
* - 按 intent + url 匹配本地工作流
* - 优先级:本地 > 云端 > LLM
*/
import * as fs from "fs";
import * as path from "path";
import type { LobsterWorkflow } from "./types.js";
export interface LocalWorkflow {
intent: string;
url: string;
workflow: LobsterWorkflow;
createdAt: number;
updatedAt: number;
successCount: number;
failureCount: number;
}
export interface LocalStoreConfig {
storageDir: string;
enabled: boolean;
}
const DEFAULT_STORAGE_DIR = path.join(process.env.HOME || "/root", ".openclaw", "workflows");
/**
* 初始化本地存储目录
*/
export function initLocalStore(config: LocalStoreConfig): void {
if (!config.enabled) return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* 保存工作流到本地
*/
export function saveLocalWorkflow(
config: LocalStoreConfig,
intent: string,
url: string,
workflow: LobsterWorkflow
): void {
if (!config.enabled) return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
// 确保目录存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
let existing: LocalWorkflow | null = null;
if (fs.existsSync(filePath)) {
try {
existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch {
existing = null;
}
}
const data: LocalWorkflow = {
intent,
url,
workflow,
createdAt: existing?.createdAt || Date.now(),
updatedAt: Date.now(),
successCount: (existing?.successCount || 0) + 1,
failureCount: existing?.failureCount || 0,
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
/**
* 从本地检索工作流
*/
export function findLocalWorkflow(
config: LocalStoreConfig,
intent: string,
url: string
): LobsterWorkflow | null {
if (!config.enabled) return null;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
// 精确匹配
const exactKey = generateKey(intent, url);
const exactPath = path.join(dir, `exactKey.json`);
if (fs.existsSync(exactPath)) {
try {
const data: LocalWorkflow = JSON.parse(fs.readFileSync(exactPath, "utf-8"));
return data.workflow;
} catch {
return null;
}
}
// 模糊匹配 - 按 intent 前缀
try {
const files = fs.readdirSync(dir);
const intentLower = intent.toLowerCase();
for (const file of files) {
if (!file.endsWith(".json")) continue;
const filePath = path.join(dir, file);
try {
const data: LocalWorkflow = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (data.intent.toLowerCase().includes(intentLower) ||
intentLower.includes(data.intent.toLowerCase())) {
return data.workflow;
}
} catch {
continue;
}
}
} catch {
// 目录不存在
}
return null;
}
/**
* 记录工作流失败
*/
export function recordLocalFailure(
config: LocalStoreConfig,
intent: string,
url: string
): void {
if (!config.enabled) return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
if (!fs.existsSync(filePath)) return;
try {
const data: LocalWorkflow = JSON.parse(fs.readFileSync(filePath, "utf-8"));
data.failureCount += 1;
data.updatedAt = Date.now();
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
} catch {
// 忽略错误
}
}
/**
* 列出所有本地工作流
*/
export function listLocalWorkflows(config: LocalStoreConfig): LocalWorkflow[] {
if (!config.enabled) return [];
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const workflows: LocalWorkflow[] = [];
if (!fs.existsSync(dir)) return workflows;
try {
const files = fs.readdirSync(dir);
for (const file of files) {
if (!file.endsWith(".json")) continue;
const filePath = path.join(dir, file);
try {
const data: LocalWorkflow = JSON.parse(fs.readFileSync(filePath, "utf-8"));
workflows.push(data);
} catch {
continue;
}
}
} catch {
// 忽略错误
}
return workflows;
}
/**
* 删除本地工作流
*/
export function deleteLocalWorkflow(
config: LocalStoreConfig,
intent: string,
url: string
): boolean {
if (!config.enabled) return false;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
}
/**
* 生成唯一键名
*/
function generateKey(intent: string, url: string): string {
const combined = `intent:url`;
// 简单哈希
let hash = 0;
for (let i = 0; i < combined.length; i++) {
const char = combined.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
FILE:src/sanitizer.ts
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
interface SanitizeResult {
sanitized: string;
extractedArgs: Map<string, ArgDefinition>;
}
interface ArgDefinition {
type: "string" | "number" | "boolean";
placeholder: string;
originalPattern: string;
}
const PII_RULES: Array<{
name: string;
pattern: RegExp;
argType: "string" | "number";
}> = [
{
name: "email",
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
argType: "string",
},
{
name: "phone",
pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
argType: "string",
},
{
name: "card",
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
argType: "string",
},
{
name: "ssn",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
argType: "string",
},
{
name: "password",
pattern: /(?:password|passwd|pwd)\s*[:=]\s*\S+/gi,
argType: "string",
},
{
name: "api_key",
pattern: /(?:api[_-]?key|token|secret)\s*[:=]\s*[A-Za-z0-9_\-]{16,}/gi,
argType: "string",
},
{
name: "ip_address",
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
argType: "string",
},
];
// Common user-input field names that likely contain personal data
const SENSITIVE_FIELD_NAMES = new Set([
"username",
"user_name",
"first_name",
"last_name",
"full_name",
"name",
"email",
"phone",
"address",
"street",
"city",
"zip",
"zipcode",
"postal_code",
"ssn",
"social_security",
"credit_card",
"card_number",
"cvv",
"expiry",
"password",
"passwd",
"secret",
"token",
"api_key",
"dob",
"date_of_birth",
"birthday",
]);
export function sanitizeTrace(raw: string): SanitizeResult {
let result = raw;
const extractedArgs = new Map<string, ArgDefinition>();
let argCounter = 0;
// Pass 1: Regex-based PII detection
for (const rule of PII_RULES) {
const matches = result.match(rule.pattern);
if (matches) {
for (const match of new Set(matches)) {
const argName = `rule.name_argCounter++`;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
extractedArgs.set(argName, {
type: rule.argType,
placeholder,
originalPattern: rule.name,
});
result = result.replaceAll(match, placeholder);
}
}
rule.pattern.lastIndex = 0;
}
return { sanitized: result, extractedArgs };
}
export function sanitizeActionArgs(
args: Record<string, unknown>
): { sanitized: Record<string, unknown>; extractedArgs: Map<string, ArgDefinition> } {
const allExtracted = new Map<string, ArgDefinition>();
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(args)) {
const lowerKey = key.toLowerCase();
// If the field name itself is sensitive, replace the value entirely
if (SENSITIVE_FIELD_NAMES.has(lowerKey)) {
const argName = lowerKey;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
allExtracted.set(argName, {
type: typeof value === "number" ? "number" : "string",
placeholder,
originalPattern: `field:key`,
});
sanitized[key] = placeholder;
continue;
}
// If the value is a string, run PII regex sanitization
if (typeof value === "string") {
const { sanitized: cleanValue, extractedArgs } = sanitizeTrace(value);
sanitized[key] = cleanValue;
for (const [k, v] of extractedArgs) {
allExtracted.set(k, v);
}
} else {
sanitized[key] = value;
}
}
return { sanitized, extractedArgs: allExtracted };
}
FILE:src/trace-compiler.ts
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
import { sanitizeActionArgs } from "./sanitizer.js";
import type {
ActionTrace,
LobsterWorkflow,
LobsterStep,
LobsterArgs,
} from "./types.js";
export interface CompileResult {
workflow: LobsterWorkflow;
argCount: number;
}
export function compileTrace(
intentName: string,
actions: ActionTrace[]
): CompileResult {
const steps: LobsterStep[] = [];
const collectedArgs: LobsterArgs = {};
let stepIndex = 0;
for (const action of actions) {
// Skip failed actions — we only want the successful path
if (!action.result.success) continue;
// Skip non-browser actions (internal bookkeeping, etc.)
if (!isBrowserAction(action)) continue;
// Sanitize args to strip PII and extract Lobster arguments
const { sanitized, extractedArgs } = sanitizeActionArgs(action.args);
// Register extracted args in the workflow args definition
for (const [argName, argDef] of extractedArgs) {
if (!collectedArgs[argName]) {
collectedArgs[argName] = {
type: argDef.type,
required: true,
};
}
}
// Build the Lobster command string
const command = buildCommand(action.tool, action.action, sanitized);
const step: LobsterStep = {
id: `step_stepIndex++`,
command,
timeout_ms: inferTimeout(action),
};
// Merge consecutive waits
if (steps.length > 0 && isWaitStep(step) && isWaitStep(steps[steps.length - 1])) {
const prev = steps[steps.length - 1];
prev.timeout_ms = (prev.timeout_ms || 1000) + (step.timeout_ms || 1000);
continue;
}
steps.push(step);
}
// Optimize: remove trailing waits
while (steps.length > 0 && isWaitStep(steps[steps.length - 1])) {
steps.pop();
}
const workflow: LobsterWorkflow = {
name: intentToWorkflowName(intentName),
args: collectedArgs,
steps,
};
return { workflow, argCount: Object.keys(collectedArgs).length };
}
function isBrowserAction(action: ActionTrace): boolean {
const browserTools = [
"browser.click",
"browser.type",
"browser.fill",
"browser.select",
"browser.check",
"browser.navigate",
"browser.scroll",
"browser.wait",
"browser.hover",
"browser.press",
"browser.evaluate",
];
const fullAction = `action.tool.action.action`;
return browserTools.some((bt) => fullAction.startsWith(bt));
}
function buildCommand(
tool: string,
action: string,
args: Record<string, unknown>
): string {
const argsStr = Object.entries(args)
.map(([k, v]) => {
if (typeof v === "string") return `k="v"`;
return `k=v`;
})
.join(" ");
return `openclaw.invoke tool.action argsStr`.trim();
}
function inferTimeout(action: ActionTrace): number {
const actionName = `action.tool.action.action`;
if (actionName.includes("navigate")) return 10000;
if (actionName.includes("wait")) return 5000;
if (actionName.includes("click")) return 3000;
if (actionName.includes("type") || actionName.includes("fill")) return 2000;
return 3000;
}
function isWaitStep(step: LobsterStep): boolean {
return step.command.includes("browser.wait");
}
function intentToWorkflowName(intent: string): string {
return intent
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.trim()
.replace(/\s+/g, "_")
.slice(0, 64);
}
FILE:src/types.ts
// ===== OpenClaw SDK Types (Skill-side interfaces) =====
export interface OpenClawContext {
workspace: WorkspaceAPI;
browser: BrowserAPI;
lobster: LobsterAPI;
sessions: SessionsAPI;
gateway: GatewayAPI;
config: SkillConfig;
logger: Logger;
}
export interface WorkspaceAPI {
getNodeId(): string;
}
export interface BrowserAPI {
getCurrentUrl(): Promise<string>;
getDomSkeletonHash(): Promise<string>;
invoke(action: string, args: Record<string, unknown>): Promise<BrowserResult>;
}
export interface BrowserResult {
success: boolean;
error?: string;
data?: unknown;
}
export interface LobsterAPI {
execute(workflow: LobsterWorkflow): Promise<LobsterExecutionResult>;
validate(workflow: LobsterWorkflow): boolean;
}
export interface LobsterExecutionResult {
success: boolean;
steps_completed: number;
total_steps: number;
error?: string;
failed_step_id?: string;
}
export interface SessionsAPI {
getHistory(sessionId: string): Promise<SessionHistory>;
getCurrentSessionId(): string;
}
export interface SessionHistory {
session_id: string;
intent: string;
actions: ActionTrace[];
status: "success" | "failure" | "in_progress";
started_at: string;
completed_at?: string;
}
export interface ActionTrace {
tool: string;
action: string;
args: Record<string, unknown>;
result: { success: boolean; error?: string };
timestamp: string;
}
export interface GatewayAPI {
passthrough(): void;
respond(message: string): void;
}
export interface SkillConfig {
get<T>(key: string): T;
}
export interface Logger {
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
}
// ===== Lobster Workflow Types =====
export interface LobsterStep {
id: string;
command: string;
env?: Record<string, string>;
timeout_ms?: number;
}
export interface LobsterArgs {
[key: string]: {
type: "string" | "number" | "boolean";
default?: string | number | boolean;
required?: boolean;
};
}
export interface LobsterWorkflow {
name: string;
args: LobsterArgs;
steps: LobsterStep[];
}
// ===== Cloud API Types =====
export interface MatchRequest {
intent: string;
intent_embedding?: number[];
url: string;
dom_skeleton_hash?: string;
node_id: string;
}
export interface MatchResponse {
hit: boolean;
macro?: {
macro_id: string;
trigger_context: {
intent_embedding: number[];
url_regex: string;
dom_skeleton_hash?: string;
};
lobster_workflow: LobsterWorkflow;
metadata: {
author_node: string;
success_rate: number;
success_count: number;
failure_count: number;
last_verified: string;
created_at: string;
};
status: string;
};
match_score?: number;
match_method?: string;
}
export interface ContributeRequest {
node_id: string;
intent: string;
url: string;
dom_skeleton_hash?: string;
lobster_workflow: LobsterWorkflow;
intent_embedding?: number[];
session_id: string;
}
export interface ContributeResponse {
accepted: boolean;
macro_id?: string;
reason?: string;
}
export interface ReportFailureRequest {
macro_id: string;
node_id: string;
error_type: "selector_not_found" | "timeout" | "unexpected_state" | "other";
error_detail?: string;
}
FILE:tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
自动缓存并复用本地成功工作流,优先本地执行节省Token,支持断网使用和云端备份共享。
# TOKEN SOP - 让你的 Token 翻倍用
---
## 🚀 你是不是经常遇到这种情况?
> "这个任务明明上周做过,怎么又要重新跑?"
> "一个简单操作,又要消耗我宝贵的 Token!"
> "看着余额一点点减少,心疼啊..."
**别担心,你不是一个人!**
---
## 💡 痛点分析
根据调研,80% 的 Agent 用户都遇到过:
| 痛点 | 心理感受 |
|------|---------|
| 重复任务重复执行 | 浪费时间 |
| Token 不知不觉耗尽 | 钱包疼心 |
| 相同操作要等很久 | 效率低下 |
---
## 🎯 解决方案:TOKEN SOP
**一款让你省 Token 的神器!**
### 核心功能
```
第一次:执行任务 → 消耗 Token
↓
自动保存工作流到本地
↓
第二次:直接调用本地工作流 → 0 Token 消耗!
```
### 相当于什么?
| 对比 | 传统方式 | TOKEN SOP |
|------|---------|-----------|
| 重复任务 | 每次消耗 Token | **一次消耗,终身免费** |
| 执行速度 | 每次重新探索 | **秒级响应** |
| 离线可用 | ❌ | ✅ 断网也能用 |
---
## 🧠 心理学设计
### 1. 损失厌恶
> "我已经花出去的 Token,如果能省下来,相当于赚钱!"
### 2. 即时满足
> "安装后立即生效,马上就能看到效果!"
### 3. 社交证明
> "1000+ 节点已经在用,工作流共享让每个人都受益"
### 4. 隐私安全
> "工作流保存在本地,不上传敏感数据,安全放心"
---
## 📦 安装理由总结
| 理由 | 适合人群 |
|------|---------|
| 省钱 | 所有 Token 付费用户 |
| 高效 | 需要重复操作的用户 |
| 省心 | 不想等待探索的用户 |
| 离线 | 网络不稳定的环境 |
---
## 🏃 立刻行动
**安装 TOKEN SOP,让你的 Agent 变身为:**
- 更省钱 💰
- 更快速 ⚡
- 更智能 🧠
**一次配置,终身受益!**
---
## 配置
| 配置 | 默认值 | 说明 |
|------|--------|------|
| enabled | true | 启用/禁用技能 |
| local_store_enabled | true | 启用本地缓存 |
| local_store_dir | ~/.openclaw/workflows | 本地存储目录 |
| auto_contribute | true | 自动贡献到云端 |
| cloud_endpoint | https://api.ainclaw.com | 云端 API 地址 |
| timeout_ms | 300 | 云端超时时间(毫秒) |
FILE:README.md
# ClawMind Skill
Local Skill for OpenClaw - Lobster Workflow Interceptor.
**One node explores, all nodes benefit.**
## Features
- **Interceptor**: Query cloud for cached workflows before LLM exploration
- **Trace Compiler**: Convert session traces to reusable Lobster workflows
- **PII Sanitizer**: Local-first privacy protection
## Hooks
- `on_intent_received` → `interceptIntent` - Query cloud, replay workflow if hit
- `on_session_complete` → `onSessionComplete` - Compile and contribute successful sessions
## Quick Start
```bash
cd skill
npm install
npm run build
# Install into OpenClaw
```
## Configuration
See `skill.json` for configurable options.
FILE:dist/client.d.ts
import type { TraceEvent } from "./interceptor";
export interface ClawMindConfig {
apiUrl: string;
nodeId: string;
timeout?: number;
}
export declare class ClawMindClient {
private config;
constructor(config: ClawMindConfig);
contribute(data: {
intent: string;
url: string;
domSkeletonHash?: string;
lobsterWorkflow: {
steps: TraceEvent[];
};
sessionId?: string;
}): Promise<any>;
match(data: {
intent: string;
url: string;
domSkeletonHash?: string;
}): Promise<any>;
reportSuccess(macroId: string): Promise<any>;
reportFailure(macroId: string, failedStepIndex?: number, errorType?: string, domSnapshot?: string): Promise<any>;
}
FILE:dist/client.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClawMindClient = void 0;
class ClawMindClient {
config;
constructor(config) {
this.config = { timeout: 30000, ...config };
}
async contribute(data) {
const response = await fetch(`this.config.apiUrl/v1/contribute`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...data, nodeId: this.config.nodeId }),
signal: AbortSignal.timeout(this.config.timeout),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Contribution failed: error.error`);
}
return response.json();
}
async match(data) {
const response = await fetch(`this.config.apiUrl/v1/match`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
signal: AbortSignal.timeout(this.config.timeout),
});
if (response.status === 404)
return null;
if (!response.ok) {
const error = await response.json();
throw new Error(`Match failed: error.error`);
}
return response.json();
}
async reportSuccess(macroId) {
const response = await fetch(`this.config.apiUrl/v1/feedback`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ macroId, nodeId: this.config.nodeId, success: true }),
signal: AbortSignal.timeout(this.config.timeout),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Feedback failed: error.error`);
}
return response.json();
}
async reportFailure(macroId, failedStepIndex, errorType, domSnapshot) {
const response = await fetch(`this.config.apiUrl/v1/feedback`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
macroId,
nodeId: this.config.nodeId,
success: false,
failedStepIndex,
errorType,
domSnapshot,
}),
signal: AbortSignal.timeout(this.config.timeout),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Feedback failed: error.error`);
}
return response.json();
}
}
exports.ClawMindClient = ClawMindClient;
//# sourceMappingURL=client.js.map
FILE:dist/cloud-client.d.ts
import type { MatchRequest, MatchResponse, ContributeRequest, ContributeResponse, ReportFailureRequest, Logger } from "./types.js";
export declare class CloudClient {
private endpoint;
private timeoutMs;
private logger;
constructor(endpoint: string, timeoutMs: number, logger: Logger);
match(req: MatchRequest): Promise<MatchResponse | null>;
contribute(req: ContributeRequest): Promise<ContributeResponse | null>;
reportFailure(req: ReportFailureRequest): Promise<void>;
}
FILE:dist/cloud-client.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudClient = void 0;
const undici_1 = require("undici");
class CloudClient {
endpoint;
timeoutMs;
logger;
constructor(endpoint, timeoutMs, logger) {
this.endpoint = endpoint.replace(/\/$/, "");
this.timeoutMs = timeoutMs;
this.logger = logger;
}
async match(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/match`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: this.timeoutMs,
bodyTimeout: this.timeoutMs,
});
if (statusCode !== 200) {
this.logger.warn(`Cloud match returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.debug(`Cloud match failed (expected on timeout/offline): err`);
return null;
}
}
async contribute(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/contribute`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 5000,
bodyTimeout: 5000,
});
if (statusCode !== 201 && statusCode !== 409) {
this.logger.warn(`Cloud contribute returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.warn(`Cloud contribute failed: err`);
return null;
}
}
async reportFailure(req) {
try {
await (0, undici_1.request)(`this.endpoint/v1/lobsters/report_failure`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 3000,
bodyTimeout: 3000,
});
}
catch (err) {
this.logger.debug(`Cloud report_failure failed: err`);
}
}
}
exports.CloudClient = CloudClient;
FILE:dist/index.d.ts
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
export { interceptIntent, onSessionComplete } from "./interceptor.js";
FILE:dist/index.js
"use strict";
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.onSessionComplete = exports.interceptIntent = void 0;
var interceptor_js_1 = require("./interceptor.js");
Object.defineProperty(exports, "interceptIntent", { enumerable: true, get: function () { return interceptor_js_1.interceptIntent; } });
Object.defineProperty(exports, "onSessionComplete", { enumerable: true, get: function () { return interceptor_js_1.onSessionComplete; } });
FILE:dist/intent-parser.d.ts
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
export interface ParsedIntent {
raw: string;
normalized: string;
domain: string;
action_verb: string;
object_noun: string;
}
export declare function parseIntent(raw: string, url: string): ParsedIntent;
FILE:dist/intent-parser.js
"use strict";
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseIntent = parseIntent;
const ACTION_VERBS = new Set([
"click", "type", "fill", "submit", "select", "check", "uncheck",
"toggle", "scroll", "navigate", "open", "close", "search",
"login", "logout", "sign_in", "sign_up", "register",
"download", "upload", "delete", "remove", "add", "create",
"edit", "update", "save", "cancel", "confirm", "accept",
"reject", "deny", "approve", "buy", "purchase", "order",
"subscribe", "unsubscribe", "bookmark", "share", "copy",
"paste", "drag", "drop", "hover", "wait", "refresh",
]);
function parseIntent(raw, url) {
const normalized = raw
.toLowerCase()
.trim()
.replace(/\s+/g, " ")
.replace(/[^\w\s]/g, "");
const words = normalized.split(" ");
// Extract action verb
let actionVerb = "interact";
for (const word of words) {
if (ACTION_VERBS.has(word)) {
actionVerb = word;
break;
}
}
// Extract object noun (first noun-like word after the verb)
const verbIndex = words.indexOf(actionVerb);
const objectWords = words.slice(verbIndex + 1).filter((w) => w.length > 2 && !["the", "a", "an", "in", "on", "at", "to", "for", "with"].includes(w));
const objectNoun = objectWords.join("_") || "element";
// Extract domain from URL
let domain = "unknown";
try {
domain = new URL(url).hostname.replace("www.", "");
}
catch {
// ignore
}
return {
raw,
normalized,
domain,
action_verb: actionVerb,
object_noun: objectNoun,
};
}
FILE:dist/interceptor.d.ts
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Query local store first (skip LLM, save tokens)
* 3. If local miss → query ClawMind Cloud
* 4. If cloud hit → execute the Lobster workflow directly
* 5. If cloud miss → passthrough to normal OpenClaw flow
* 6. On success → save to local store + contribute to cloud
* 7. On failure → report failure for circuit breaker tracking
*/
import type { OpenClawContext } from "./types.js";
export declare function interceptIntent(ctx: OpenClawContext): Promise<void>;
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and:
* 1. Saves to local store (for faster retrieval next time)
* 2. Contributes to cloud (for sharing with other nodes)
*/
export declare function onSessionComplete(ctx: OpenClawContext): Promise<void>;
FILE:dist/interceptor.js
"use strict";
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Query local store first (skip LLM, save tokens)
* 3. If local miss → query ClawMind Cloud
* 4. If cloud hit → execute the Lobster workflow directly
* 5. If cloud miss → passthrough to normal OpenClaw flow
* 6. On success → save to local store + contribute to cloud
* 7. On failure → report failure for circuit breaker tracking
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.interceptIntent = interceptIntent;
exports.onSessionComplete = onSessionComplete;
const cloud_client_js_1 = require("./cloud-client.js");
const intent_parser_js_1 = require("./intent-parser.js");
const trace_compiler_js_1 = require("./trace-compiler.js");
const local_store_js_1 = require("./local-store.js");
async function interceptIntent(ctx) {
const { browser, lobster, sessions, gateway, workspace, config, logger } = ctx;
// Check if skill is enabled
if (!config.get("enabled")) {
gateway.passthrough();
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
const intent = history.intent;
if (!intent) {
gateway.passthrough();
return;
}
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
const parsed = (0, intent_parser_js_1.parseIntent)(intent, url);
logger.info(`[ClawMind] Intercepting intent: "parsed.normalized" on parsed.domain`);
// ===== Step 1: Query local store first (save tokens!) =====
const localWorkflow = (0, local_store_js_1.findLocalWorkflow)({ storageDir: "", enabled: true }, // 使用默认配置
parsed.normalized, url);
if (localWorkflow) {
logger.info(`[ClawMind] Local workflow found, executing directly (saves tokens!)`);
// Validate and execute local workflow
if (!lobster.validate(localWorkflow)) {
logger.warn(`[ClawMind] Local workflow validation failed, falling back`);
}
else {
try {
const execResult = await lobster.execute(localWorkflow);
if (execResult.success) {
logger.info(`[ClawMind] Local workflow executed successfully`);
gateway.respond(`✅ Done via local cached workflow. execResult.steps_completed steps replayed.`);
return;
}
else {
logger.warn(`[ClawMind] Local workflow failed: execResult.error`);
(0, local_store_js_1.recordLocalFailure)({ storageDir: "", enabled: true }, parsed.normalized, url);
}
}
catch (err) {
logger.error(`[ClawMind] Local workflow execution error: err`);
}
}
}
// ===== Step 2: Query cloud if local miss =====
const cloudEndpoint = config.get("cloud_endpoint");
const timeoutMs = config.get("timeout_ms");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, timeoutMs, logger);
let matchResult = null;
try {
matchResult = await client.match({
intent: parsed.normalized,
url,
dom_skeleton_hash: domHash,
node_id: nodeId,
});
}
catch {
// Cloud unavailable — graceful degradation, just passthrough
logger.debug("[ClawMind] Cloud unreachable, passing through");
gateway.passthrough();
return;
}
// No match — let OpenClaw handle it normally
if (!matchResult || !matchResult.hit || !matchResult.macro) {
logger.info("[ClawMind] No macro match, passing through to OpenClaw");
gateway.passthrough();
return;
}
const macro = matchResult.macro;
logger.info(`[ClawMind] Cloud match found: macro.macro_id (score: matchResult.match_score, method: matchResult.match_method)`);
// Validate the workflow before execution
if (!lobster.validate(macro.lobster_workflow)) {
logger.warn(`[ClawMind] Workflow validation failed for macro.macro_id`);
gateway.passthrough();
return;
}
// Execute the Lobster workflow
let execResult;
try {
execResult = await lobster.execute(macro.lobster_workflow);
}
catch (err) {
logger.error(`[ClawMind] Lobster execution threw: err`);
// Report failure to cloud (fire-and-forget)
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: "other",
error_detail: String(err),
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
return;
}
if (execResult.success) {
logger.info(`[ClawMind] Macro macro.macro_id executed successfully (execResult.steps_completed/execResult.total_steps steps)`);
gateway.respond(`✅ Done via ClawMind cached workflow (macro.macro_id). ` +
`execResult.steps_completed steps replayed.`);
}
else {
logger.warn(`[ClawMind] Macro macro.macro_id failed at step execResult.failed_step_id: execResult.error`);
// Map error to error_type
const errorType = mapErrorType(execResult.error || "");
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: errorType,
error_detail: execResult.error,
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
}
}
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and:
* 1. Saves to local store (for faster retrieval next time)
* 2. Contributes to cloud (for sharing with other nodes)
*/
async function onSessionComplete(ctx) {
const { browser, sessions, workspace, config, logger } = ctx;
if (!config.get("enabled") || !config.get("auto_contribute")) {
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
// Only contribute successful sessions with meaningful actions
if (history.status !== "success")
return;
if (history.actions.length < 2)
return;
const intent = history.intent;
if (!intent)
return;
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
// Compile the trace into a Lobster workflow
const { workflow, argCount } = (0, trace_compiler_js_1.compileTrace)(intent, history.actions);
// Skip trivial workflows (single step, no real logic)
if (workflow.steps.length < 2) {
logger.debug("[ClawMind] Workflow too trivial to contribute, skipping");
return;
}
logger.info(`[ClawMind] Saving workflow "workflow.name" (workflow.steps.length steps, argCount args)`);
// ===== Step 1: Save to local store =====
try {
(0, local_store_js_1.saveLocalWorkflow)({ storageDir: "", enabled: true }, intent, url, workflow);
logger.info("[ClawMind] Workflow saved to local store");
}
catch (err) {
logger.warn(`[ClawMind] Failed to save local workflow: err`);
}
// ===== Step 2: Contribute to cloud =====
const cloudEndpoint = config.get("cloud_endpoint");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, 5000, logger);
try {
const result = await client.contribute({
node_id: nodeId,
intent,
url,
dom_skeleton_hash: domHash,
lobster_workflow: workflow,
session_id: sessionId,
});
if (result?.accepted) {
logger.info(`[ClawMind] Cloud contribution accepted: result.macro_id`);
}
else {
logger.debug(`[ClawMind] Cloud contribution not accepted: result?.reason`);
}
}
catch (err) {
logger.debug(`[ClawMind] Cloud contribution failed: err`);
}
}
function mapErrorType(error) {
const lower = error.toLowerCase();
if (lower.includes("selector") || lower.includes("not found") || lower.includes("no element")) {
return "selector_not_found";
}
if (lower.includes("timeout") || lower.includes("timed out")) {
return "timeout";
}
if (lower.includes("unexpected") || lower.includes("state")) {
return "unexpected_state";
}
return "other";
}
FILE:dist/local-store.d.ts
/**
* Local Workflow Store - 保存和检索本地工作流
*
* 功能:
* - 保存成功的工作流到本地文件
* - 按 intent + url 匹配本地工作流
* - 优先级:本地 > 云端 > LLM
*/
import type { LobsterWorkflow } from "./types.js";
export interface LocalWorkflow {
intent: string;
url: string;
workflow: LobsterWorkflow;
createdAt: number;
updatedAt: number;
successCount: number;
failureCount: number;
}
export interface LocalStoreConfig {
storageDir: string;
enabled: boolean;
}
/**
* 初始化本地存储目录
*/
export declare function initLocalStore(config: LocalStoreConfig): void;
/**
* 保存工作流到本地
*/
export declare function saveLocalWorkflow(config: LocalStoreConfig, intent: string, url: string, workflow: LobsterWorkflow): void;
/**
* 从本地检索工作流
*/
export declare function findLocalWorkflow(config: LocalStoreConfig, intent: string, url: string): LobsterWorkflow | null;
/**
* 记录工作流失败
*/
export declare function recordLocalFailure(config: LocalStoreConfig, intent: string, url: string): void;
/**
* 列出所有本地工作流
*/
export declare function listLocalWorkflows(config: LocalStoreConfig): LocalWorkflow[];
/**
* 删除本地工作流
*/
export declare function deleteLocalWorkflow(config: LocalStoreConfig, intent: string, url: string): boolean;
FILE:dist/local-store.js
"use strict";
/**
* Local Workflow Store - 保存和检索本地工作流
*
* 功能:
* - 保存成功的工作流到本地文件
* - 按 intent + url 匹配本地工作流
* - 优先级:本地 > 云端 > LLM
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.initLocalStore = initLocalStore;
exports.saveLocalWorkflow = saveLocalWorkflow;
exports.findLocalWorkflow = findLocalWorkflow;
exports.recordLocalFailure = recordLocalFailure;
exports.listLocalWorkflows = listLocalWorkflows;
exports.deleteLocalWorkflow = deleteLocalWorkflow;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const DEFAULT_STORAGE_DIR = path.join(process.env.HOME || "/root", ".openclaw", "workflows");
/**
* 初始化本地存储目录
*/
function initLocalStore(config) {
if (!config.enabled)
return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* 保存工作流到本地
*/
function saveLocalWorkflow(config, intent, url, workflow) {
if (!config.enabled)
return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
// 确保目录存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
let existing = null;
if (fs.existsSync(filePath)) {
try {
existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
}
catch {
existing = null;
}
}
const data = {
intent,
url,
workflow,
createdAt: existing?.createdAt || Date.now(),
updatedAt: Date.now(),
successCount: (existing?.successCount || 0) + 1,
failureCount: existing?.failureCount || 0,
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
/**
* 从本地检索工作流
*/
function findLocalWorkflow(config, intent, url) {
if (!config.enabled)
return null;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
// 精确匹配
const exactKey = generateKey(intent, url);
const exactPath = path.join(dir, `exactKey.json`);
if (fs.existsSync(exactPath)) {
try {
const data = JSON.parse(fs.readFileSync(exactPath, "utf-8"));
return data.workflow;
}
catch {
return null;
}
}
// 模糊匹配 - 按 intent 前缀
try {
const files = fs.readdirSync(dir);
const intentLower = intent.toLowerCase();
for (const file of files) {
if (!file.endsWith(".json"))
continue;
const filePath = path.join(dir, file);
try {
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (data.intent.toLowerCase().includes(intentLower) ||
intentLower.includes(data.intent.toLowerCase())) {
return data.workflow;
}
}
catch {
continue;
}
}
}
catch {
// 目录不存在
}
return null;
}
/**
* 记录工作流失败
*/
function recordLocalFailure(config, intent, url) {
if (!config.enabled)
return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
if (!fs.existsSync(filePath))
return;
try {
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
data.failureCount += 1;
data.updatedAt = Date.now();
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
catch {
// 忽略错误
}
}
/**
* 列出所有本地工作流
*/
function listLocalWorkflows(config) {
if (!config.enabled)
return [];
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const workflows = [];
if (!fs.existsSync(dir))
return workflows;
try {
const files = fs.readdirSync(dir);
for (const file of files) {
if (!file.endsWith(".json"))
continue;
const filePath = path.join(dir, file);
try {
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
workflows.push(data);
}
catch {
continue;
}
}
}
catch {
// 忽略错误
}
return workflows;
}
/**
* 删除本地工作流
*/
function deleteLocalWorkflow(config, intent, url) {
if (!config.enabled)
return false;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
}
/**
* 生成唯一键名
*/
function generateKey(intent, url) {
const combined = `intent:url`;
// 简单哈希
let hash = 0;
for (let i = 0; i < combined.length; i++) {
const char = combined.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
FILE:dist/sanitizer.d.ts
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
interface SanitizeResult {
sanitized: string;
extractedArgs: Map<string, ArgDefinition>;
}
interface ArgDefinition {
type: "string" | "number" | "boolean";
placeholder: string;
originalPattern: string;
}
export declare function sanitizeTrace(raw: string): SanitizeResult;
export declare function sanitizeActionArgs(args: Record<string, unknown>): {
sanitized: Record<string, unknown>;
extractedArgs: Map<string, ArgDefinition>;
};
export {};
FILE:dist/sanitizer.js
"use strict";
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeTrace = sanitizeTrace;
exports.sanitizeActionArgs = sanitizeActionArgs;
const PII_RULES = [
{
name: "email",
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
argType: "string",
},
{
name: "phone",
pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
argType: "string",
},
{
name: "card",
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
argType: "string",
},
{
name: "ssn",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
argType: "string",
},
{
name: "password",
pattern: /(?:password|passwd|pwd)\s*[:=]\s*\S+/gi,
argType: "string",
},
{
name: "api_key",
pattern: /(?:api[_-]?key|token|secret)\s*[:=]\s*[A-Za-z0-9_\-]{16,}/gi,
argType: "string",
},
{
name: "ip_address",
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
argType: "string",
},
];
// Common user-input field names that likely contain personal data
const SENSITIVE_FIELD_NAMES = new Set([
"username",
"user_name",
"first_name",
"last_name",
"full_name",
"name",
"email",
"phone",
"address",
"street",
"city",
"zip",
"zipcode",
"postal_code",
"ssn",
"social_security",
"credit_card",
"card_number",
"cvv",
"expiry",
"password",
"passwd",
"secret",
"token",
"api_key",
"dob",
"date_of_birth",
"birthday",
]);
function sanitizeTrace(raw) {
let result = raw;
const extractedArgs = new Map();
let argCounter = 0;
// Pass 1: Regex-based PII detection
for (const rule of PII_RULES) {
const matches = result.match(rule.pattern);
if (matches) {
for (const match of new Set(matches)) {
const argName = `rule.name_argCounter++`;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
extractedArgs.set(argName, {
type: rule.argType,
placeholder,
originalPattern: rule.name,
});
result = result.replaceAll(match, placeholder);
}
}
rule.pattern.lastIndex = 0;
}
return { sanitized: result, extractedArgs };
}
function sanitizeActionArgs(args) {
const allExtracted = new Map();
const sanitized = {};
for (const [key, value] of Object.entries(args)) {
const lowerKey = key.toLowerCase();
// If the field name itself is sensitive, replace the value entirely
if (SENSITIVE_FIELD_NAMES.has(lowerKey)) {
const argName = lowerKey;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
allExtracted.set(argName, {
type: typeof value === "number" ? "number" : "string",
placeholder,
originalPattern: `field:key`,
});
sanitized[key] = placeholder;
continue;
}
// If the value is a string, run PII regex sanitization
if (typeof value === "string") {
const { sanitized: cleanValue, extractedArgs } = sanitizeTrace(value);
sanitized[key] = cleanValue;
for (const [k, v] of extractedArgs) {
allExtracted.set(k, v);
}
}
else {
sanitized[key] = value;
}
}
return { sanitized, extractedArgs: allExtracted };
}
FILE:dist/trace-compiler.d.ts
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
import type { ActionTrace, LobsterWorkflow } from "./types.js";
export interface CompileResult {
workflow: LobsterWorkflow;
argCount: number;
}
export declare function compileTrace(intentName: string, actions: ActionTrace[]): CompileResult;
FILE:dist/trace-compiler.js
"use strict";
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileTrace = compileTrace;
const sanitizer_js_1 = require("./sanitizer.js");
function compileTrace(intentName, actions) {
const steps = [];
const collectedArgs = {};
let stepIndex = 0;
for (const action of actions) {
// Skip failed actions — we only want the successful path
if (!action.result.success)
continue;
// Skip non-browser actions (internal bookkeeping, etc.)
if (!isBrowserAction(action))
continue;
// Sanitize args to strip PII and extract Lobster arguments
const { sanitized, extractedArgs } = (0, sanitizer_js_1.sanitizeActionArgs)(action.args);
// Register extracted args in the workflow args definition
for (const [argName, argDef] of extractedArgs) {
if (!collectedArgs[argName]) {
collectedArgs[argName] = {
type: argDef.type,
required: true,
};
}
}
// Build the Lobster command string
const command = buildCommand(action.tool, action.action, sanitized);
const step = {
id: `step_stepIndex++`,
command,
timeout_ms: inferTimeout(action),
};
// Merge consecutive waits
if (steps.length > 0 && isWaitStep(step) && isWaitStep(steps[steps.length - 1])) {
const prev = steps[steps.length - 1];
prev.timeout_ms = (prev.timeout_ms || 1000) + (step.timeout_ms || 1000);
continue;
}
steps.push(step);
}
// Optimize: remove trailing waits
while (steps.length > 0 && isWaitStep(steps[steps.length - 1])) {
steps.pop();
}
const workflow = {
name: intentToWorkflowName(intentName),
args: collectedArgs,
steps,
};
return { workflow, argCount: Object.keys(collectedArgs).length };
}
function isBrowserAction(action) {
const browserTools = [
"browser.click",
"browser.type",
"browser.fill",
"browser.select",
"browser.check",
"browser.navigate",
"browser.scroll",
"browser.wait",
"browser.hover",
"browser.press",
"browser.evaluate",
];
const fullAction = `action.tool.action.action`;
return browserTools.some((bt) => fullAction.startsWith(bt));
}
function buildCommand(tool, action, args) {
const argsStr = Object.entries(args)
.map(([k, v]) => {
if (typeof v === "string")
return `k="v"`;
return `k=v`;
})
.join(" ");
return `openclaw.invoke tool.action argsStr`.trim();
}
function inferTimeout(action) {
const actionName = `action.tool.action.action`;
if (actionName.includes("navigate"))
return 10000;
if (actionName.includes("wait"))
return 5000;
if (actionName.includes("click"))
return 3000;
if (actionName.includes("type") || actionName.includes("fill"))
return 2000;
return 3000;
}
function isWaitStep(step) {
return step.command.includes("browser.wait");
}
function intentToWorkflowName(intent) {
return intent
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.trim()
.replace(/\s+/g, "_")
.slice(0, 64);
}
FILE:dist/types.d.ts
export interface OpenClawContext {
workspace: WorkspaceAPI;
browser: BrowserAPI;
lobster: LobsterAPI;
sessions: SessionsAPI;
gateway: GatewayAPI;
config: SkillConfig;
logger: Logger;
}
export interface WorkspaceAPI {
getNodeId(): string;
}
export interface BrowserAPI {
getCurrentUrl(): Promise<string>;
getDomSkeletonHash(): Promise<string>;
invoke(action: string, args: Record<string, unknown>): Promise<BrowserResult>;
}
export interface BrowserResult {
success: boolean;
error?: string;
data?: unknown;
}
export interface LobsterAPI {
execute(workflow: LobsterWorkflow): Promise<LobsterExecutionResult>;
validate(workflow: LobsterWorkflow): boolean;
}
export interface LobsterExecutionResult {
success: boolean;
steps_completed: number;
total_steps: number;
error?: string;
failed_step_id?: string;
}
export interface SessionsAPI {
getHistory(sessionId: string): Promise<SessionHistory>;
getCurrentSessionId(): string;
}
export interface SessionHistory {
session_id: string;
intent: string;
actions: ActionTrace[];
status: "success" | "failure" | "in_progress";
started_at: string;
completed_at?: string;
}
export interface ActionTrace {
tool: string;
action: string;
args: Record<string, unknown>;
result: {
success: boolean;
error?: string;
};
timestamp: string;
}
export interface GatewayAPI {
passthrough(): void;
respond(message: string): void;
}
export interface SkillConfig {
get<T>(key: string): T;
}
export interface Logger {
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
}
export interface LobsterStep {
id: string;
command: string;
env?: Record<string, string>;
timeout_ms?: number;
}
export interface LobsterArgs {
[key: string]: {
type: "string" | "number" | "boolean";
default?: string | number | boolean;
required?: boolean;
};
}
export interface LobsterWorkflow {
name: string;
args: LobsterArgs;
steps: LobsterStep[];
}
export interface MatchRequest {
intent: string;
intent_embedding?: number[];
url: string;
dom_skeleton_hash?: string;
node_id: string;
}
export interface MatchResponse {
hit: boolean;
macro?: {
macro_id: string;
trigger_context: {
intent_embedding: number[];
url_regex: string;
dom_skeleton_hash?: string;
};
lobster_workflow: LobsterWorkflow;
metadata: {
author_node: string;
success_rate: number;
success_count: number;
failure_count: number;
last_verified: string;
created_at: string;
};
status: string;
};
match_score?: number;
match_method?: string;
}
export interface ContributeRequest {
node_id: string;
intent: string;
url: string;
dom_skeleton_hash?: string;
lobster_workflow: LobsterWorkflow;
intent_embedding?: number[];
session_id: string;
}
export interface ContributeResponse {
accepted: boolean;
macro_id?: string;
reason?: string;
}
export interface ReportFailureRequest {
macro_id: string;
node_id: string;
error_type: "selector_not_found" | "timeout" | "unexpected_state" | "other";
error_detail?: string;
}
FILE:dist/types.js
"use strict";
// ===== OpenClaw SDK Types (Skill-side interfaces) =====
Object.defineProperty(exports, "__esModule", { value: true });
FILE:package-lock.json
{
"name": "token-sop",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "token-sop",
"version": "1.0.1",
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}
FILE:package.json
{
"name": "token-sop",
"version": "5.6.0",
"description": "本地工作流缓存技能 - 自动保存成功的工作流到本地,下次执行相同任务时自动调取,节省 Token",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"typescript": "^5.7.0",
"@types/node": "^22.0.0"
},
"keywords": ["openclaw", "workflow", "automation", "token", "sop", "cache", "local"]
}
FILE:skill.json
{
"name": "token-sop",
"version": "5.6.0",
"display_name": "TOKEN SOP",
"description": "本地工作流缓存技能。自动保存成功的工作流到本地,下次执行相同任务时自动调取,节省 Token。支持本地优先 + 云端备份。",
"author": "clawmind-team",
"homepage": "https://clawhub.dev/skills/token-sop",
"license": "MIT",
"entry": "dist/index.js",
"permissions": [
"browser",
"lobster",
"sessions_history",
"network"
],
"hooks": {
"on_intent_received": "interceptIntent",
"on_session_complete": "onSessionComplete"
},
"config": {
"cloud_endpoint": {
"type": "string",
"default": "https://api.ainclaw.com",
"description": "TOKEN SOP Cloud API endpoint"
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable/disable TOKEN SOP interception"
},
"auto_contribute": {
"type": "boolean",
"default": true,
"description": "Automatically contribute successful workflows to cloud"
},
"timeout_ms": {
"type": "number",
"default": 300,
"description": "Cloud API timeout in milliseconds"
},
"local_store_enabled": {
"type": "boolean",
"default": true,
"description": "Enable local workflow storage for faster retrieval and token saving"
},
"local_store_dir": {
"type": "string",
"default": "~/.openclaw/workflows",
"description": "Local workflow storage directory"
}
}
}
FILE:src/cloud-client.ts
import { request } from "undici";
import type {
MatchRequest,
MatchResponse,
ContributeRequest,
ContributeResponse,
ReportFailureRequest,
Logger,
} from "./types.js";
export class CloudClient {
private endpoint: string;
private timeoutMs: number;
private logger: Logger;
constructor(endpoint: string, timeoutMs: number, logger: Logger) {
this.endpoint = endpoint.replace(/\/$/, "");
this.timeoutMs = timeoutMs;
this.logger = logger;
}
async match(req: MatchRequest): Promise<MatchResponse | null> {
try {
const { statusCode, body } = await request(
`this.endpoint/v1/lobsters/match`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: this.timeoutMs,
bodyTimeout: this.timeoutMs,
}
);
if (statusCode !== 200) {
this.logger.warn(`Cloud match returned statusCode`);
return null;
}
return (await body.json()) as MatchResponse;
} catch (err) {
this.logger.debug(`Cloud match failed (expected on timeout/offline): err`);
return null;
}
}
async contribute(req: ContributeRequest): Promise<ContributeResponse | null> {
try {
const { statusCode, body } = await request(
`this.endpoint/v1/lobsters/contribute`,
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 5000,
bodyTimeout: 5000,
}
);
if (statusCode !== 201 && statusCode !== 409) {
this.logger.warn(`Cloud contribute returned statusCode`);
return null;
}
return (await body.json()) as ContributeResponse;
} catch (err) {
this.logger.warn(`Cloud contribute failed: err`);
return null;
}
}
async reportFailure(req: ReportFailureRequest): Promise<void> {
try {
await request(`this.endpoint/v1/lobsters/report_failure`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 3000,
bodyTimeout: 3000,
});
} catch (err) {
this.logger.debug(`Cloud report_failure failed: err`);
}
}
}
FILE:src/index.ts
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
export { interceptIntent, onSessionComplete } from "./interceptor.js";
FILE:src/intent-parser.ts
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
export interface ParsedIntent {
raw: string;
normalized: string;
domain: string;
action_verb: string;
object_noun: string;
}
const ACTION_VERBS = new Set([
"click", "type", "fill", "submit", "select", "check", "uncheck",
"toggle", "scroll", "navigate", "open", "close", "search",
"login", "logout", "sign_in", "sign_up", "register",
"download", "upload", "delete", "remove", "add", "create",
"edit", "update", "save", "cancel", "confirm", "accept",
"reject", "deny", "approve", "buy", "purchase", "order",
"subscribe", "unsubscribe", "bookmark", "share", "copy",
"paste", "drag", "drop", "hover", "wait", "refresh",
]);
export function parseIntent(raw: string, url: string): ParsedIntent {
const normalized = raw
.toLowerCase()
.trim()
.replace(/\s+/g, " ")
.replace(/[^\w\s]/g, "");
const words = normalized.split(" ");
// Extract action verb
let actionVerb = "interact";
for (const word of words) {
if (ACTION_VERBS.has(word)) {
actionVerb = word;
break;
}
}
// Extract object noun (first noun-like word after the verb)
const verbIndex = words.indexOf(actionVerb);
const objectWords = words.slice(verbIndex + 1).filter(
(w) => w.length > 2 && !["the", "a", "an", "in", "on", "at", "to", "for", "with"].includes(w)
);
const objectNoun = objectWords.join("_") || "element";
// Extract domain from URL
let domain = "unknown";
try {
domain = new URL(url).hostname.replace("www.", "");
} catch {
// ignore
}
return {
raw,
normalized,
domain,
action_verb: actionVerb,
object_noun: objectNoun,
};
}
FILE:src/interceptor.ts
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Query local store first (skip LLM, save tokens)
* 3. If local miss → query ClawMind Cloud
* 4. If cloud hit → execute the Lobster workflow directly
* 5. If cloud miss → passthrough to normal OpenClaw flow
* 6. On success → save to local store + contribute to cloud
* 7. On failure → report failure for circuit breaker tracking
*/
import { CloudClient } from "./cloud-client.js";
import { parseIntent } from "./intent-parser.js";
import { compileTrace } from "./trace-compiler.js";
import {
initLocalStore,
saveLocalWorkflow,
findLocalWorkflow,
recordLocalFailure,
LocalStoreConfig,
} from "./local-store.js";
import type {
OpenClawContext,
MatchResponse,
LobsterExecutionResult,
} from "./types.js";
export async function interceptIntent(ctx: OpenClawContext): Promise<void> {
const { browser, lobster, sessions, gateway, workspace, config, logger } = ctx;
// Check if skill is enabled
if (!config.get<boolean>("enabled")) {
gateway.passthrough();
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
const intent = history.intent;
if (!intent) {
gateway.passthrough();
return;
}
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
const parsed = parseIntent(intent, url);
logger.info(`[ClawMind] Intercepting intent: "parsed.normalized" on parsed.domain`);
// ===== Step 1: Query local store first (save tokens!) =====
const localWorkflow = findLocalWorkflow(
{ storageDir: "", enabled: true }, // 使用默认配置
parsed.normalized,
url
);
if (localWorkflow) {
logger.info(`[ClawMind] Local workflow found, executing directly (saves tokens!)`);
// Validate and execute local workflow
if (!lobster.validate(localWorkflow)) {
logger.warn(`[ClawMind] Local workflow validation failed, falling back`);
} else {
try {
const execResult = await lobster.execute(localWorkflow);
if (execResult.success) {
logger.info(`[ClawMind] Local workflow executed successfully`);
gateway.respond(
`✅ Done via local cached workflow. execResult.steps_completed steps replayed.`
);
return;
} else {
logger.warn(`[ClawMind] Local workflow failed: execResult.error`);
recordLocalFailure({ storageDir: "", enabled: true }, parsed.normalized, url);
}
} catch (err) {
logger.error(`[ClawMind] Local workflow execution error: err`);
}
}
}
// ===== Step 2: Query cloud if local miss =====
const cloudEndpoint = config.get<string>("cloud_endpoint");
const timeoutMs = config.get<number>("timeout_ms");
const client = new CloudClient(cloudEndpoint, timeoutMs, logger);
let matchResult: MatchResponse | null = null;
try {
matchResult = await client.match({
intent: parsed.normalized,
url,
dom_skeleton_hash: domHash,
node_id: nodeId,
});
} catch {
// Cloud unavailable — graceful degradation, just passthrough
logger.debug("[ClawMind] Cloud unreachable, passing through");
gateway.passthrough();
return;
}
// No match — let OpenClaw handle it normally
if (!matchResult || !matchResult.hit || !matchResult.macro) {
logger.info("[ClawMind] No macro match, passing through to OpenClaw");
gateway.passthrough();
return;
}
const macro = matchResult.macro;
logger.info(
`[ClawMind] Cloud match found: macro.macro_id (score: matchResult.match_score, method: matchResult.match_method)`
);
// Validate the workflow before execution
if (!lobster.validate(macro.lobster_workflow)) {
logger.warn(`[ClawMind] Workflow validation failed for macro.macro_id`);
gateway.passthrough();
return;
}
// Execute the Lobster workflow
let execResult: LobsterExecutionResult;
try {
execResult = await lobster.execute(macro.lobster_workflow);
} catch (err) {
logger.error(`[ClawMind] Lobster execution threw: err`);
// Report failure to cloud (fire-and-forget)
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: "other",
error_detail: String(err),
}).catch(() => {});
// Fall back to normal OpenClaw flow
gateway.passthrough();
return;
}
if (execResult.success) {
logger.info(
`[ClawMind] Macro macro.macro_id executed successfully (execResult.steps_completed/execResult.total_steps steps)`
);
gateway.respond(
`✅ Done via ClawMind cached workflow (macro.macro_id). ` +
`execResult.steps_completed steps replayed.`
);
} else {
logger.warn(
`[ClawMind] Macro macro.macro_id failed at step execResult.failed_step_id: execResult.error`
);
// Map error to error_type
const errorType = mapErrorType(execResult.error || "");
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: errorType,
error_detail: execResult.error,
}).catch(() => {});
// Fall back to normal OpenClaw flow
gateway.passthrough();
}
}
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and:
* 1. Saves to local store (for faster retrieval next time)
* 2. Contributes to cloud (for sharing with other nodes)
*/
export async function onSessionComplete(ctx: OpenClawContext): Promise<void> {
const { browser, sessions, workspace, config, logger } = ctx;
if (!config.get<boolean>("enabled") || !config.get<boolean>("auto_contribute")) {
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
// Only contribute successful sessions with meaningful actions
if (history.status !== "success") return;
if (history.actions.length < 2) return;
const intent = history.intent;
if (!intent) return;
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
// Compile the trace into a Lobster workflow
const { workflow, argCount } = compileTrace(intent, history.actions);
// Skip trivial workflows (single step, no real logic)
if (workflow.steps.length < 2) {
logger.debug("[ClawMind] Workflow too trivial to contribute, skipping");
return;
}
logger.info(
`[ClawMind] Saving workflow "workflow.name" (workflow.steps.length steps, argCount args)`
);
// ===== Step 1: Save to local store =====
try {
saveLocalWorkflow(
{ storageDir: "", enabled: true },
intent,
url,
workflow
);
logger.info("[ClawMind] Workflow saved to local store");
} catch (err) {
logger.warn(`[ClawMind] Failed to save local workflow: err`);
}
// ===== Step 2: Contribute to cloud =====
const cloudEndpoint = config.get<string>("cloud_endpoint");
const client = new CloudClient(cloudEndpoint, 5000, logger);
try {
const result = await client.contribute({
node_id: nodeId,
intent,
url,
dom_skeleton_hash: domHash,
lobster_workflow: workflow,
session_id: sessionId,
});
if (result?.accepted) {
logger.info(`[ClawMind] Cloud contribution accepted: result.macro_id`);
} else {
logger.debug(`[ClawMind] Cloud contribution not accepted: result?.reason`);
}
} catch (err) {
logger.debug(`[ClawMind] Cloud contribution failed: err`);
}
}
function mapErrorType(
error: string
): "selector_not_found" | "timeout" | "unexpected_state" | "other" {
const lower = error.toLowerCase();
if (lower.includes("selector") || lower.includes("not found") || lower.includes("no element")) {
return "selector_not_found";
}
if (lower.includes("timeout") || lower.includes("timed out")) {
return "timeout";
}
if (lower.includes("unexpected") || lower.includes("state")) {
return "unexpected_state";
}
return "other";
}
FILE:src/local-store.ts
/**
* Local Workflow Store - 保存和检索本地工作流
*
* 功能:
* - 保存成功的工作流到本地文件
* - 按 intent + url 匹配本地工作流
* - 优先级:本地 > 云端 > LLM
*/
import * as fs from "fs";
import * as path from "path";
import type { LobsterWorkflow } from "./types.js";
export interface LocalWorkflow {
intent: string;
url: string;
workflow: LobsterWorkflow;
createdAt: number;
updatedAt: number;
successCount: number;
failureCount: number;
}
export interface LocalStoreConfig {
storageDir: string;
enabled: boolean;
}
const DEFAULT_STORAGE_DIR = path.join(process.env.HOME || "/root", ".openclaw", "workflows");
/**
* 初始化本地存储目录
*/
export function initLocalStore(config: LocalStoreConfig): void {
if (!config.enabled) return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* 保存工作流到本地
*/
export function saveLocalWorkflow(
config: LocalStoreConfig,
intent: string,
url: string,
workflow: LobsterWorkflow
): void {
if (!config.enabled) return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
// 确保目录存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
let existing: LocalWorkflow | null = null;
if (fs.existsSync(filePath)) {
try {
existing = JSON.parse(fs.readFileSync(filePath, "utf-8"));
} catch {
existing = null;
}
}
const data: LocalWorkflow = {
intent,
url,
workflow,
createdAt: existing?.createdAt || Date.now(),
updatedAt: Date.now(),
successCount: (existing?.successCount || 0) + 1,
failureCount: existing?.failureCount || 0,
};
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
/**
* 从本地检索工作流
*/
export function findLocalWorkflow(
config: LocalStoreConfig,
intent: string,
url: string
): LobsterWorkflow | null {
if (!config.enabled) return null;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
// 精确匹配
const exactKey = generateKey(intent, url);
const exactPath = path.join(dir, `exactKey.json`);
if (fs.existsSync(exactPath)) {
try {
const data: LocalWorkflow = JSON.parse(fs.readFileSync(exactPath, "utf-8"));
return data.workflow;
} catch {
return null;
}
}
// 模糊匹配 - 按 intent 前缀
try {
const files = fs.readdirSync(dir);
const intentLower = intent.toLowerCase();
for (const file of files) {
if (!file.endsWith(".json")) continue;
const filePath = path.join(dir, file);
try {
const data: LocalWorkflow = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (data.intent.toLowerCase().includes(intentLower) ||
intentLower.includes(data.intent.toLowerCase())) {
return data.workflow;
}
} catch {
continue;
}
}
} catch {
// 目录不存在
}
return null;
}
/**
* 记录工作流失败
*/
export function recordLocalFailure(
config: LocalStoreConfig,
intent: string,
url: string
): void {
if (!config.enabled) return;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
if (!fs.existsSync(filePath)) return;
try {
const data: LocalWorkflow = JSON.parse(fs.readFileSync(filePath, "utf-8"));
data.failureCount += 1;
data.updatedAt = Date.now();
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
} catch {
// 忽略错误
}
}
/**
* 列出所有本地工作流
*/
export function listLocalWorkflows(config: LocalStoreConfig): LocalWorkflow[] {
if (!config.enabled) return [];
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const workflows: LocalWorkflow[] = [];
if (!fs.existsSync(dir)) return workflows;
try {
const files = fs.readdirSync(dir);
for (const file of files) {
if (!file.endsWith(".json")) continue;
const filePath = path.join(dir, file);
try {
const data: LocalWorkflow = JSON.parse(fs.readFileSync(filePath, "utf-8"));
workflows.push(data);
} catch {
continue;
}
}
} catch {
// 忽略错误
}
return workflows;
}
/**
* 删除本地工作流
*/
export function deleteLocalWorkflow(
config: LocalStoreConfig,
intent: string,
url: string
): boolean {
if (!config.enabled) return false;
const dir = config.storageDir || DEFAULT_STORAGE_DIR;
const key = generateKey(intent, url);
const filePath = path.join(dir, `key.json`);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
}
/**
* 生成唯一键名
*/
function generateKey(intent: string, url: string): string {
const combined = `intent:url`;
// 简单哈希
let hash = 0;
for (let i = 0; i < combined.length; i++) {
const char = combined.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
FILE:src/sanitizer.ts
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
interface SanitizeResult {
sanitized: string;
extractedArgs: Map<string, ArgDefinition>;
}
interface ArgDefinition {
type: "string" | "number" | "boolean";
placeholder: string;
originalPattern: string;
}
const PII_RULES: Array<{
name: string;
pattern: RegExp;
argType: "string" | "number";
}> = [
{
name: "email",
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
argType: "string",
},
{
name: "phone",
pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
argType: "string",
},
{
name: "card",
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
argType: "string",
},
{
name: "ssn",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
argType: "string",
},
{
name: "password",
pattern: /(?:password|passwd|pwd)\s*[:=]\s*\S+/gi,
argType: "string",
},
{
name: "api_key",
pattern: /(?:api[_-]?key|token|secret)\s*[:=]\s*[A-Za-z0-9_\-]{16,}/gi,
argType: "string",
},
{
name: "ip_address",
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
argType: "string",
},
];
// Common user-input field names that likely contain personal data
const SENSITIVE_FIELD_NAMES = new Set([
"username",
"user_name",
"first_name",
"last_name",
"full_name",
"name",
"email",
"phone",
"address",
"street",
"city",
"zip",
"zipcode",
"postal_code",
"ssn",
"social_security",
"credit_card",
"card_number",
"cvv",
"expiry",
"password",
"passwd",
"secret",
"token",
"api_key",
"dob",
"date_of_birth",
"birthday",
]);
export function sanitizeTrace(raw: string): SanitizeResult {
let result = raw;
const extractedArgs = new Map<string, ArgDefinition>();
let argCounter = 0;
// Pass 1: Regex-based PII detection
for (const rule of PII_RULES) {
const matches = result.match(rule.pattern);
if (matches) {
for (const match of new Set(matches)) {
const argName = `rule.name_argCounter++`;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
extractedArgs.set(argName, {
type: rule.argType,
placeholder,
originalPattern: rule.name,
});
result = result.replaceAll(match, placeholder);
}
}
rule.pattern.lastIndex = 0;
}
return { sanitized: result, extractedArgs };
}
export function sanitizeActionArgs(
args: Record<string, unknown>
): { sanitized: Record<string, unknown>; extractedArgs: Map<string, ArgDefinition> } {
const allExtracted = new Map<string, ArgDefinition>();
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(args)) {
const lowerKey = key.toLowerCase();
// If the field name itself is sensitive, replace the value entirely
if (SENSITIVE_FIELD_NAMES.has(lowerKey)) {
const argName = lowerKey;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
allExtracted.set(argName, {
type: typeof value === "number" ? "number" : "string",
placeholder,
originalPattern: `field:key`,
});
sanitized[key] = placeholder;
continue;
}
// If the value is a string, run PII regex sanitization
if (typeof value === "string") {
const { sanitized: cleanValue, extractedArgs } = sanitizeTrace(value);
sanitized[key] = cleanValue;
for (const [k, v] of extractedArgs) {
allExtracted.set(k, v);
}
} else {
sanitized[key] = value;
}
}
return { sanitized, extractedArgs: allExtracted };
}
FILE:src/trace-compiler.ts
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
import { sanitizeActionArgs } from "./sanitizer.js";
import type {
ActionTrace,
LobsterWorkflow,
LobsterStep,
LobsterArgs,
} from "./types.js";
export interface CompileResult {
workflow: LobsterWorkflow;
argCount: number;
}
export function compileTrace(
intentName: string,
actions: ActionTrace[]
): CompileResult {
const steps: LobsterStep[] = [];
const collectedArgs: LobsterArgs = {};
let stepIndex = 0;
for (const action of actions) {
// Skip failed actions — we only want the successful path
if (!action.result.success) continue;
// Skip non-browser actions (internal bookkeeping, etc.)
if (!isBrowserAction(action)) continue;
// Sanitize args to strip PII and extract Lobster arguments
const { sanitized, extractedArgs } = sanitizeActionArgs(action.args);
// Register extracted args in the workflow args definition
for (const [argName, argDef] of extractedArgs) {
if (!collectedArgs[argName]) {
collectedArgs[argName] = {
type: argDef.type,
required: true,
};
}
}
// Build the Lobster command string
const command = buildCommand(action.tool, action.action, sanitized);
const step: LobsterStep = {
id: `step_stepIndex++`,
command,
timeout_ms: inferTimeout(action),
};
// Merge consecutive waits
if (steps.length > 0 && isWaitStep(step) && isWaitStep(steps[steps.length - 1])) {
const prev = steps[steps.length - 1];
prev.timeout_ms = (prev.timeout_ms || 1000) + (step.timeout_ms || 1000);
continue;
}
steps.push(step);
}
// Optimize: remove trailing waits
while (steps.length > 0 && isWaitStep(steps[steps.length - 1])) {
steps.pop();
}
const workflow: LobsterWorkflow = {
name: intentToWorkflowName(intentName),
args: collectedArgs,
steps,
};
return { workflow, argCount: Object.keys(collectedArgs).length };
}
function isBrowserAction(action: ActionTrace): boolean {
const browserTools = [
"browser.click",
"browser.type",
"browser.fill",
"browser.select",
"browser.check",
"browser.navigate",
"browser.scroll",
"browser.wait",
"browser.hover",
"browser.press",
"browser.evaluate",
];
const fullAction = `action.tool.action.action`;
return browserTools.some((bt) => fullAction.startsWith(bt));
}
function buildCommand(
tool: string,
action: string,
args: Record<string, unknown>
): string {
const argsStr = Object.entries(args)
.map(([k, v]) => {
if (typeof v === "string") return `k="v"`;
return `k=v`;
})
.join(" ");
return `openclaw.invoke tool.action argsStr`.trim();
}
function inferTimeout(action: ActionTrace): number {
const actionName = `action.tool.action.action`;
if (actionName.includes("navigate")) return 10000;
if (actionName.includes("wait")) return 5000;
if (actionName.includes("click")) return 3000;
if (actionName.includes("type") || actionName.includes("fill")) return 2000;
return 3000;
}
function isWaitStep(step: LobsterStep): boolean {
return step.command.includes("browser.wait");
}
function intentToWorkflowName(intent: string): string {
return intent
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.trim()
.replace(/\s+/g, "_")
.slice(0, 64);
}
FILE:src/types.ts
// ===== OpenClaw SDK Types (Skill-side interfaces) =====
export interface OpenClawContext {
workspace: WorkspaceAPI;
browser: BrowserAPI;
lobster: LobsterAPI;
sessions: SessionsAPI;
gateway: GatewayAPI;
config: SkillConfig;
logger: Logger;
}
export interface WorkspaceAPI {
getNodeId(): string;
}
export interface BrowserAPI {
getCurrentUrl(): Promise<string>;
getDomSkeletonHash(): Promise<string>;
invoke(action: string, args: Record<string, unknown>): Promise<BrowserResult>;
}
export interface BrowserResult {
success: boolean;
error?: string;
data?: unknown;
}
export interface LobsterAPI {
execute(workflow: LobsterWorkflow): Promise<LobsterExecutionResult>;
validate(workflow: LobsterWorkflow): boolean;
}
export interface LobsterExecutionResult {
success: boolean;
steps_completed: number;
total_steps: number;
error?: string;
failed_step_id?: string;
}
export interface SessionsAPI {
getHistory(sessionId: string): Promise<SessionHistory>;
getCurrentSessionId(): string;
}
export interface SessionHistory {
session_id: string;
intent: string;
actions: ActionTrace[];
status: "success" | "failure" | "in_progress";
started_at: string;
completed_at?: string;
}
export interface ActionTrace {
tool: string;
action: string;
args: Record<string, unknown>;
result: { success: boolean; error?: string };
timestamp: string;
}
export interface GatewayAPI {
passthrough(): void;
respond(message: string): void;
}
export interface SkillConfig {
get<T>(key: string): T;
}
export interface Logger {
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
}
// ===== Lobster Workflow Types =====
export interface LobsterStep {
id: string;
command: string;
env?: Record<string, string>;
timeout_ms?: number;
}
export interface LobsterArgs {
[key: string]: {
type: "string" | "number" | "boolean";
default?: string | number | boolean;
required?: boolean;
};
}
export interface LobsterWorkflow {
name: string;
args: LobsterArgs;
steps: LobsterStep[];
}
// ===== Cloud API Types =====
export interface MatchRequest {
intent: string;
intent_embedding?: number[];
url: string;
dom_skeleton_hash?: string;
node_id: string;
}
export interface MatchResponse {
hit: boolean;
macro?: {
macro_id: string;
trigger_context: {
intent_embedding: number[];
url_regex: string;
dom_skeleton_hash?: string;
};
lobster_workflow: LobsterWorkflow;
metadata: {
author_node: string;
success_rate: number;
success_count: number;
failure_count: number;
last_verified: string;
created_at: string;
};
status: string;
};
match_score?: number;
match_method?: string;
}
export interface ContributeRequest {
node_id: string;
intent: string;
url: string;
dom_skeleton_hash?: string;
lobster_workflow: LobsterWorkflow;
intent_embedding?: number[];
session_id: string;
}
export interface ContributeResponse {
accepted: boolean;
macro_id?: string;
reason?: string;
}
export interface ReportFailureRequest {
macro_id: string;
node_id: string;
error_type: "selector_not_found" | "timeout" | "unexpected_state" | "other";
error_detail?: string;
}
FILE:tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost.
---
name: clawmind
description: "Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost."
metadata:
openclaw:
emoji: "🧠"
---
# ClawMind
**One agent explores, all agents benefit.**
A crowdsourced workflow registry that caches successful automation patterns, letting you skip LLM inference entirely when a matching workflow exists.
## Why Use This?
### 1. Save Real Money
Traditional approach: LLM explores and reasons through every step, burning tokens on trial-and-error.
Our approach: Query the cloud for a cached workflow. If found, execute directly. **Zero inference cost.**
**Token savings example (10-step browser task):**
- Traditional: ~5000 tokens
- Workflow Cache: ~800 tokens
- **Savings: 80%+**
The more complex the task and the more you repeat it, the more you save.
### 2. Skip the Debugging Hell
The painful part of AI automation isn't writing the script—it's the endless debugging when:
- The website changes its layout
- Selectors break unexpectedly
- Edge cases you didn't anticipate
**ClawMind solves this:**
- Every successful workflow from any agent is cached
- When websites change, cached workflows auto-update
- You never debug the same problem twice
### 3. Platform Agnostic
Works with any Claw/Lobster engine. One workflow, all platforms. Automatic syntax adaptation.
## How It Works
```
User Intent → Query Cloud → Match Found?
↓ Yes ↓ No
Execute Now Normal Flow
(1 second) (LLM reasons)
↓ ↓
Success! Success → Contribute
```
**One agent's success becomes every agent's shortcut.**
## Features
### Interceptor
Queries the cloud before LLM inference. On match, replays the cached workflow directly.
### Trace Compiler
Converts successful session traces into reusable Lobster workflows automatically.
### PII Sanitizer
Local-first privacy. All sensitive data stays local. Only workflow patterns are shared.
## Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `cloud_endpoint` | string | `https://api.workflowcache.dev` | Cloud API endpoint |
| `enabled` | boolean | `true` | Enable/disable interception |
| `auto_contribute` | boolean | `true` | Auto-contribute successful workflows |
| `timeout_ms` | number | `300` | API timeout (ms) |
## Installation
```bash
npx clawhub install ainclaw-mind
```
Or manually:
```bash
cd ~/.qclaw/workspace/skills/ainclaw-mind
npm install
npm run build
```
## Security
- Full PII sanitization pipeline
- No account credentials ever uploaded
- Multi-node security validation on all workflows
- Malicious injection detection and blocking
## Who Is This For?
- **Heavy AI users** — Daily automation, high token bills
- **Cost-conscious developers** — Every token saved is money saved
- **Automation enthusiasts** — Stop reinventing wheels
- **Efficiency maximalists** — Why reason when you can replay?
## License
MIT-0 — Free to use, modify, and redistribute. No attribution required.
---
**Tags:** `#AI-efficiency` `#token-saver` `#automation` `#crowdsourced` `#workflow-cache`
FILE:dist/cloud-client.d.ts
import type { MatchRequest, MatchResponse, ContributeRequest, ContributeResponse, ReportFailureRequest, Logger } from "./types.js";
export declare class CloudClient {
private endpoint;
private timeoutMs;
private logger;
constructor(endpoint: string, timeoutMs: number, logger: Logger);
match(req: MatchRequest): Promise<MatchResponse | null>;
contribute(req: ContributeRequest): Promise<ContributeResponse | null>;
reportFailure(req: ReportFailureRequest): Promise<void>;
}
FILE:dist/cloud-client.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudClient = void 0;
const undici_1 = require("undici");
class CloudClient {
endpoint;
timeoutMs;
logger;
constructor(endpoint, timeoutMs, logger) {
this.endpoint = endpoint.replace(/\/$/, "");
this.timeoutMs = timeoutMs;
this.logger = logger;
}
async match(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/match`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: this.timeoutMs,
bodyTimeout: this.timeoutMs,
});
if (statusCode !== 200) {
this.logger.warn(`Cloud match returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.debug(`Cloud match failed (expected on timeout/offline): err`);
return null;
}
}
async contribute(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/contribute`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 5000,
bodyTimeout: 5000,
});
if (statusCode !== 201 && statusCode !== 409) {
this.logger.warn(`Cloud contribute returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.warn(`Cloud contribute failed: err`);
return null;
}
}
async reportFailure(req) {
try {
await (0, undici_1.request)(`this.endpoint/v1/lobsters/report_failure`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 3000,
bodyTimeout: 3000,
});
}
catch (err) {
this.logger.debug(`Cloud report_failure failed: err`);
}
}
}
exports.CloudClient = CloudClient;
FILE:dist/index.d.ts
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
export { interceptIntent, onSessionComplete } from "./interceptor.js";
FILE:dist/index.js
"use strict";
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.onSessionComplete = exports.interceptIntent = void 0;
var interceptor_js_1 = require("./interceptor.js");
Object.defineProperty(exports, "interceptIntent", { enumerable: true, get: function () { return interceptor_js_1.interceptIntent; } });
Object.defineProperty(exports, "onSessionComplete", { enumerable: true, get: function () { return interceptor_js_1.onSessionComplete; } });
FILE:dist/intent-parser.d.ts
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
export interface ParsedIntent {
raw: string;
normalized: string;
domain: string;
action_verb: string;
object_noun: string;
}
export declare function parseIntent(raw: string, url: string): ParsedIntent;
FILE:dist/intent-parser.js
"use strict";
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseIntent = parseIntent;
const ACTION_VERBS = new Set([
"click", "type", "fill", "submit", "select", "check", "uncheck",
"toggle", "scroll", "navigate", "open", "close", "search",
"login", "logout", "sign_in", "sign_up", "register",
"download", "upload", "delete", "remove", "add", "create",
"edit", "update", "save", "cancel", "confirm", "accept",
"reject", "deny", "approve", "buy", "purchase", "order",
"subscribe", "unsubscribe", "bookmark", "share", "copy",
"paste", "drag", "drop", "hover", "wait", "refresh",
]);
function parseIntent(raw, url) {
const normalized = raw
.toLowerCase()
.trim()
.replace(/\s+/g, " ")
.replace(/[^\w\s]/g, "");
const words = normalized.split(" ");
// Extract action verb
let actionVerb = "interact";
for (const word of words) {
if (ACTION_VERBS.has(word)) {
actionVerb = word;
break;
}
}
// Extract object noun (first noun-like word after the verb)
const verbIndex = words.indexOf(actionVerb);
const objectWords = words.slice(verbIndex + 1).filter((w) => w.length > 2 && !["the", "a", "an", "in", "on", "at", "to", "for", "with"].includes(w));
const objectNoun = objectWords.join("_") || "element";
// Extract domain from URL
let domain = "unknown";
try {
domain = new URL(url).hostname.replace("www.", "");
}
catch {
// ignore
}
return {
raw,
normalized,
domain,
action_verb: actionVerb,
object_noun: objectNoun,
};
}
FILE:dist/interceptor.d.ts
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Interceptor queries ClawMind Cloud for a matching Lobster macro
* 3. If hit → execute the Lobster workflow directly (skip LLM exploration)
* 4. If miss → passthrough to normal OpenClaw flow
* 5. On success → contribute the trace back to the cloud
* 6. On failure → report failure for circuit breaker tracking
*/
import type { OpenClawContext } from "./types.js";
export declare function interceptIntent(ctx: OpenClawContext): Promise<void>;
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and contributes it.
*/
export declare function onSessionComplete(ctx: OpenClawContext): Promise<void>;
FILE:dist/interceptor.js
"use strict";
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Interceptor queries ClawMind Cloud for a matching Lobster macro
* 3. If hit → execute the Lobster workflow directly (skip LLM exploration)
* 4. If miss → passthrough to normal OpenClaw flow
* 5. On success → contribute the trace back to the cloud
* 6. On failure → report failure for circuit breaker tracking
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.interceptIntent = interceptIntent;
exports.onSessionComplete = onSessionComplete;
const cloud_client_js_1 = require("./cloud-client.js");
const intent_parser_js_1 = require("./intent-parser.js");
const trace_compiler_js_1 = require("./trace-compiler.js");
async function interceptIntent(ctx) {
const { browser, lobster, sessions, gateway, workspace, config, logger } = ctx;
// Check if skill is enabled
if (!config.get("enabled")) {
gateway.passthrough();
return;
}
const cloudEndpoint = config.get("cloud_endpoint");
const timeoutMs = config.get("timeout_ms");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, timeoutMs, logger);
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
const intent = history.intent;
if (!intent) {
gateway.passthrough();
return;
}
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
const parsed = (0, intent_parser_js_1.parseIntent)(intent, url);
logger.info(`[ClawMind] Intercepting intent: "parsed.normalized" on parsed.domain`);
// Query cloud for a matching macro
let matchResult = null;
try {
matchResult = await client.match({
intent: parsed.normalized,
url,
dom_skeleton_hash: domHash,
node_id: nodeId,
});
}
catch {
// Cloud unavailable — graceful degradation, just passthrough
logger.debug("[ClawMind] Cloud unreachable, passing through");
gateway.passthrough();
return;
}
// No match — let OpenClaw handle it normally
if (!matchResult || !matchResult.hit || !matchResult.macro) {
logger.info("[ClawMind] No macro match, passing through to OpenClaw");
gateway.passthrough();
return;
}
const macro = matchResult.macro;
logger.info(`[ClawMind] Match found: macro.macro_id (score: matchResult.match_score, method: matchResult.match_method)`);
// Validate the workflow before execution
if (!lobster.validate(macro.lobster_workflow)) {
logger.warn(`[ClawMind] Workflow validation failed for macro.macro_id`);
gateway.passthrough();
return;
}
// Execute the Lobster workflow
let execResult;
try {
execResult = await lobster.execute(macro.lobster_workflow);
}
catch (err) {
logger.error(`[ClawMind] Lobster execution threw: err`);
// Report failure to cloud (fire-and-forget)
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: "other",
error_detail: String(err),
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
return;
}
if (execResult.success) {
logger.info(`[ClawMind] Macro macro.macro_id executed successfully (execResult.steps_completed/execResult.total_steps steps)`);
gateway.respond(`✅ Done via ClawMind cached workflow (macro.macro_id). ` +
`execResult.steps_completed steps replayed.`);
}
else {
logger.warn(`[ClawMind] Macro macro.macro_id failed at step execResult.failed_step_id: execResult.error`);
// Map error to error_type
const errorType = mapErrorType(execResult.error || "");
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: errorType,
error_detail: execResult.error,
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
}
}
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and contributes it.
*/
async function onSessionComplete(ctx) {
const { browser, sessions, workspace, config, logger } = ctx;
if (!config.get("enabled") || !config.get("auto_contribute")) {
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
// Only contribute successful sessions with meaningful actions
if (history.status !== "success")
return;
if (history.actions.length < 2)
return;
const intent = history.intent;
if (!intent)
return;
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
// Compile the trace into a Lobster workflow
const { workflow, argCount } = (0, trace_compiler_js_1.compileTrace)(intent, history.actions);
// Skip trivial workflows (single step, no real logic)
if (workflow.steps.length < 2) {
logger.debug("[ClawMind] Workflow too trivial to contribute, skipping");
return;
}
logger.info(`[ClawMind] Contributing workflow "workflow.name" (workflow.steps.length steps, argCount args)`);
const cloudEndpoint = config.get("cloud_endpoint");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, 5000, logger);
try {
const result = await client.contribute({
node_id: nodeId,
intent,
url,
dom_skeleton_hash: domHash,
lobster_workflow: workflow,
session_id: sessionId,
});
if (result?.accepted) {
logger.info(`[ClawMind] Contribution accepted: result.macro_id`);
}
else {
logger.debug(`[ClawMind] Contribution not accepted: result?.reason`);
}
}
catch (err) {
logger.debug(`[ClawMind] Contribution failed: err`);
}
}
function mapErrorType(error) {
const lower = error.toLowerCase();
if (lower.includes("selector") || lower.includes("not found") || lower.includes("no element")) {
return "selector_not_found";
}
if (lower.includes("timeout") || lower.includes("timed out")) {
return "timeout";
}
if (lower.includes("unexpected") || lower.includes("state")) {
return "unexpected_state";
}
return "other";
}
FILE:dist/sanitizer.d.ts
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
interface SanitizeResult {
sanitized: string;
extractedArgs: Map<string, ArgDefinition>;
}
interface ArgDefinition {
type: "string" | "number" | "boolean";
placeholder: string;
originalPattern: string;
}
export declare function sanitizeTrace(raw: string): SanitizeResult;
export declare function sanitizeActionArgs(args: Record<string, unknown>): {
sanitized: Record<string, unknown>;
extractedArgs: Map<string, ArgDefinition>;
};
export {};
FILE:dist/sanitizer.js
"use strict";
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeTrace = sanitizeTrace;
exports.sanitizeActionArgs = sanitizeActionArgs;
const PII_RULES = [
{
name: "email",
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
argType: "string",
},
{
name: "phone",
pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
argType: "string",
},
{
name: "card",
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
argType: "string",
},
{
name: "ssn",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
argType: "string",
},
{
name: "password",
pattern: /(?:password|passwd|pwd)\s*[:=]\s*\S+/gi,
argType: "string",
},
{
name: "api_key",
pattern: /(?:api[_-]?key|token|secret)\s*[:=]\s*[A-Za-z0-9_\-]{16,}/gi,
argType: "string",
},
{
name: "ip_address",
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
argType: "string",
},
];
// Common user-input field names that likely contain personal data
const SENSITIVE_FIELD_NAMES = new Set([
"username",
"user_name",
"first_name",
"last_name",
"full_name",
"name",
"email",
"phone",
"address",
"street",
"city",
"zip",
"zipcode",
"postal_code",
"ssn",
"social_security",
"credit_card",
"card_number",
"cvv",
"expiry",
"password",
"passwd",
"secret",
"token",
"api_key",
"dob",
"date_of_birth",
"birthday",
]);
function sanitizeTrace(raw) {
let result = raw;
const extractedArgs = new Map();
let argCounter = 0;
// Pass 1: Regex-based PII detection
for (const rule of PII_RULES) {
const matches = result.match(rule.pattern);
if (matches) {
for (const match of new Set(matches)) {
const argName = `rule.name_argCounter++`;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
extractedArgs.set(argName, {
type: rule.argType,
placeholder,
originalPattern: rule.name,
});
result = result.replaceAll(match, placeholder);
}
}
rule.pattern.lastIndex = 0;
}
return { sanitized: result, extractedArgs };
}
function sanitizeActionArgs(args) {
const allExtracted = new Map();
const sanitized = {};
for (const [key, value] of Object.entries(args)) {
const lowerKey = key.toLowerCase();
// If the field name itself is sensitive, replace the value entirely
if (SENSITIVE_FIELD_NAMES.has(lowerKey)) {
const argName = lowerKey;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
allExtracted.set(argName, {
type: typeof value === "number" ? "number" : "string",
placeholder,
originalPattern: `field:key`,
});
sanitized[key] = placeholder;
continue;
}
// If the value is a string, run PII regex sanitization
if (typeof value === "string") {
const { sanitized: cleanValue, extractedArgs } = sanitizeTrace(value);
sanitized[key] = cleanValue;
for (const [k, v] of extractedArgs) {
allExtracted.set(k, v);
}
}
else {
sanitized[key] = value;
}
}
return { sanitized, extractedArgs: allExtracted };
}
FILE:dist/trace-compiler.d.ts
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
import type { ActionTrace, LobsterWorkflow } from "./types.js";
export interface CompileResult {
workflow: LobsterWorkflow;
argCount: number;
}
export declare function compileTrace(intentName: string, actions: ActionTrace[]): CompileResult;
FILE:dist/trace-compiler.js
"use strict";
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileTrace = compileTrace;
const sanitizer_js_1 = require("./sanitizer.js");
function compileTrace(intentName, actions) {
const steps = [];
const collectedArgs = {};
let stepIndex = 0;
for (const action of actions) {
// Skip failed actions — we only want the successful path
if (!action.result.success)
continue;
// Skip non-browser actions (internal bookkeeping, etc.)
if (!isBrowserAction(action))
continue;
// Sanitize args to strip PII and extract Lobster arguments
const { sanitized, extractedArgs } = (0, sanitizer_js_1.sanitizeActionArgs)(action.args);
// Register extracted args in the workflow args definition
for (const [argName, argDef] of extractedArgs) {
if (!collectedArgs[argName]) {
collectedArgs[argName] = {
type: argDef.type,
required: true,
};
}
}
// Build the Lobster command string
const command = buildCommand(action.tool, action.action, sanitized);
const step = {
id: `step_stepIndex++`,
command,
timeout_ms: inferTimeout(action),
};
// Merge consecutive waits
if (steps.length > 0 && isWaitStep(step) && isWaitStep(steps[steps.length - 1])) {
const prev = steps[steps.length - 1];
prev.timeout_ms = (prev.timeout_ms || 1000) + (step.timeout_ms || 1000);
continue;
}
steps.push(step);
}
// Optimize: remove trailing waits
while (steps.length > 0 && isWaitStep(steps[steps.length - 1])) {
steps.pop();
}
const workflow = {
name: intentToWorkflowName(intentName),
args: collectedArgs,
steps,
};
return { workflow, argCount: Object.keys(collectedArgs).length };
}
function isBrowserAction(action) {
const browserTools = [
"browser.click",
"browser.type",
"browser.fill",
"browser.select",
"browser.check",
"browser.navigate",
"browser.scroll",
"browser.wait",
"browser.hover",
"browser.press",
"browser.evaluate",
];
const fullAction = `action.tool.action.action`;
return browserTools.some((bt) => fullAction.startsWith(bt));
}
function buildCommand(tool, action, args) {
const argsStr = Object.entries(args)
.map(([k, v]) => {
if (typeof v === "string")
return `k="v"`;
return `k=v`;
})
.join(" ");
return `openclaw.invoke tool.action argsStr`.trim();
}
function inferTimeout(action) {
const actionName = `action.tool.action.action`;
if (actionName.includes("navigate"))
return 10000;
if (actionName.includes("wait"))
return 5000;
if (actionName.includes("click"))
return 3000;
if (actionName.includes("type") || actionName.includes("fill"))
return 2000;
return 3000;
}
function isWaitStep(step) {
return step.command.includes("browser.wait");
}
function intentToWorkflowName(intent) {
return intent
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.trim()
.replace(/\s+/g, "_")
.slice(0, 64);
}
FILE:dist/types.d.ts
export interface OpenClawContext {
workspace: WorkspaceAPI;
browser: BrowserAPI;
lobster: LobsterAPI;
sessions: SessionsAPI;
gateway: GatewayAPI;
config: SkillConfig;
logger: Logger;
}
export interface WorkspaceAPI {
getNodeId(): string;
}
export interface BrowserAPI {
getCurrentUrl(): Promise<string>;
getDomSkeletonHash(): Promise<string>;
invoke(action: string, args: Record<string, unknown>): Promise<BrowserResult>;
}
export interface BrowserResult {
success: boolean;
error?: string;
data?: unknown;
}
export interface LobsterAPI {
execute(workflow: LobsterWorkflow): Promise<LobsterExecutionResult>;
validate(workflow: LobsterWorkflow): boolean;
}
export interface LobsterExecutionResult {
success: boolean;
steps_completed: number;
total_steps: number;
error?: string;
failed_step_id?: string;
}
export interface SessionsAPI {
getHistory(sessionId: string): Promise<SessionHistory>;
getCurrentSessionId(): string;
}
export interface SessionHistory {
session_id: string;
intent: string;
actions: ActionTrace[];
status: "success" | "failure" | "in_progress";
started_at: string;
completed_at?: string;
}
export interface ActionTrace {
tool: string;
action: string;
args: Record<string, unknown>;
result: {
success: boolean;
error?: string;
};
timestamp: string;
}
export interface GatewayAPI {
passthrough(): void;
respond(message: string): void;
}
export interface SkillConfig {
get<T>(key: string): T;
}
export interface Logger {
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
}
export interface LobsterStep {
id: string;
command: string;
env?: Record<string, string>;
timeout_ms?: number;
}
export interface LobsterArgs {
[key: string]: {
type: "string" | "number" | "boolean";
default?: string | number | boolean;
required?: boolean;
};
}
export interface LobsterWorkflow {
name: string;
args: LobsterArgs;
steps: LobsterStep[];
}
export interface MatchRequest {
intent: string;
intent_embedding?: number[];
url: string;
dom_skeleton_hash?: string;
node_id: string;
}
export interface MatchResponse {
hit: boolean;
macro?: {
macro_id: string;
trigger_context: {
intent_embedding: number[];
url_regex: string;
dom_skeleton_hash?: string;
};
lobster_workflow: LobsterWorkflow;
metadata: {
author_node: string;
success_rate: number;
success_count: number;
failure_count: number;
last_verified: string;
created_at: string;
};
status: string;
};
match_score?: number;
match_method?: string;
}
export interface ContributeRequest {
node_id: string;
intent: string;
url: string;
dom_skeleton_hash?: string;
lobster_workflow: LobsterWorkflow;
intent_embedding?: number[];
session_id: string;
}
export interface ContributeResponse {
accepted: boolean;
macro_id?: string;
reason?: string;
}
export interface ReportFailureRequest {
macro_id: string;
node_id: string;
error_type: "selector_not_found" | "timeout" | "unexpected_state" | "other";
error_detail?: string;
}
FILE:dist/types.js
"use strict";
// ===== OpenClaw SDK Types (Skill-side interfaces) =====
Object.defineProperty(exports, "__esModule", { value: true });
FILE:package-lock.json
{
"name": "clawmind-skill",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawmind-skill",
"version": "1.0.0",
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}
FILE:package.json
{
"name": "clawmind",
"version": "1.0.5",
"displayName": "ClawMind",
"description": "Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost.",
"author": "ainclaw",
"homepage": "https://clawhub.ai/ainclaw-mind",
"license": "MIT-0",
"keywords": ["ai-efficiency", "token-saver", "automation", "crowdsourced", "workflow-cache"],
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"typescript": "^5.7.0",
"@types/node": "^22.0.0"
}
}
FILE:README.md
# Workflow Cache
**One agent explores, all agents benefit.**
A crowdsourced Lobster workflow registry that caches successful automation patterns.
## Features
- **Interceptor**: Query cloud for cached workflows before LLM exploration
- **Trace Compiler**: Convert session traces to reusable Lobster workflows
- **PII Sanitizer**: Local-first privacy protection
## Hooks
- `on_intent_received` → Query cloud, replay workflow if hit
- `on_session_complete` → Compile and contribute successful sessions
## Quick Start
```bash
cd skill
npm install
npm run build
```
## Configuration
See `skill.json` for configurable options.
## License
MIT-0 — Free to use, modify, and redistribute.
FILE:skill.json
{
"name": "workflow-cache",
"version": "1.0.3",
"display_name": "Workflow Cache",
"description": "Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost.",
"author": "ainclaw",
"homepage": "https://clawhub.ai/workflow-cache",
"license": "MIT-0",
"tags": ["ai-efficiency", "token-saver", "automation", "crowdsourced", "workflow-cache"],
"entry": "dist/index.js",
"permissions": [
"browser",
"lobster",
"sessions_history",
"network"
],
"hooks": {
"on_intent_received": "interceptIntent",
"on_session_complete": "onSessionComplete"
},
"config": {
"cloud_endpoint": {
"type": "string",
"default": "https://api.workflowcache.dev",
"description": "Cloud API endpoint for workflow cache"
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable/disable workflow cache interception"
},
"auto_contribute": {
"type": "boolean",
"default": true,
"description": "Automatically contribute successful workflows"
},
"timeout_ms": {
"type": "number",
"default": 300,
"description": "Cloud API timeout in milliseconds"
}
}
}Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost.
---
name: workflow-cache
description: "Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost."
metadata:
openclaw:
emoji: "🧠"
---
# Workflow Cache
**One agent explores, all agents benefit.**
A crowdsourced workflow registry that caches successful automation patterns, letting you skip LLM inference entirely when a matching workflow exists.
## Why Use This?
### 1. Save Real Money
Traditional approach: LLM explores and reasons through every step, burning tokens on trial-and-error.
Our approach: Query the cloud for a cached workflow. If found, execute directly. **Zero inference cost.**
**Token savings example (10-step browser task):**
- Traditional: ~5000 tokens
- Workflow Cache: ~800 tokens
- **Savings: 80%+**
The more complex the task and the more you repeat it, the more you save.
### 2. Skip the Debugging Hell
The painful part of AI automation isn't writing the script—it's the endless debugging when:
- The website changes its layout
- Selectors break unexpectedly
- Edge cases you didn't anticipate
**Workflow Cache solves this:**
- Every successful workflow from any agent is cached
- When websites change, cached workflows auto-update
- You never debug the same problem twice
### 3. Platform Agnostic
Works with any Claw/Lobster engine. One workflow, all platforms. Automatic syntax adaptation.
## How It Works
```
User Intent → Query Cloud → Match Found?
↓ Yes ↓ No
Execute Now Normal Flow
(1 second) (LLM reasons)
↓ ↓
Success! Success → Contribute
```
**One agent's success becomes every agent's shortcut.**
## Features
### Interceptor
Queries the cloud before LLM inference. On match, replays the cached workflow directly.
### Trace Compiler
Converts successful session traces into reusable Lobster workflows automatically.
### PII Sanitizer
Local-first privacy. All sensitive data stays local. Only workflow patterns are shared.
## Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `cloud_endpoint` | string | `https://api.workflowcache.dev` | Cloud API endpoint |
| `enabled` | boolean | `true` | Enable/disable interception |
| `auto_contribute` | boolean | `true` | Auto-contribute successful workflows |
| `timeout_ms` | number | `300` | API timeout (ms) |
## Installation
```bash
npx clawhub install workflow-cache
```
Or manually:
```bash
cd ~/.qclaw/workspace/skills/workflow-cache
npm install
npm run build
```
## Security
- Full PII sanitization pipeline
- No account credentials ever uploaded
- Multi-node security validation on all workflows
- Malicious injection detection and blocking
## Who Is This For?
- **Heavy AI users** — Daily automation, high token bills
- **Cost-conscious developers** — Every token saved is money saved
- **Automation enthusiasts** — Stop reinventing wheels
- **Efficiency maximalists** — Why reason when you can replay?
## License
MIT-0 — Free to use, modify, and redistribute. No attribution required.
---
**Tags:** `#AI-efficiency` `#token-saver` `#automation` `#crowdsourced` `#workflow-cache`
FILE:dist/cloud-client.d.ts
import type { MatchRequest, MatchResponse, ContributeRequest, ContributeResponse, ReportFailureRequest, Logger } from "./types.js";
export declare class CloudClient {
private endpoint;
private timeoutMs;
private logger;
constructor(endpoint: string, timeoutMs: number, logger: Logger);
match(req: MatchRequest): Promise<MatchResponse | null>;
contribute(req: ContributeRequest): Promise<ContributeResponse | null>;
reportFailure(req: ReportFailureRequest): Promise<void>;
}
FILE:dist/cloud-client.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudClient = void 0;
const undici_1 = require("undici");
class CloudClient {
endpoint;
timeoutMs;
logger;
constructor(endpoint, timeoutMs, logger) {
this.endpoint = endpoint.replace(/\/$/, "");
this.timeoutMs = timeoutMs;
this.logger = logger;
}
async match(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/match`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: this.timeoutMs,
bodyTimeout: this.timeoutMs,
});
if (statusCode !== 200) {
this.logger.warn(`Cloud match returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.debug(`Cloud match failed (expected on timeout/offline): err`);
return null;
}
}
async contribute(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/contribute`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 5000,
bodyTimeout: 5000,
});
if (statusCode !== 201 && statusCode !== 409) {
this.logger.warn(`Cloud contribute returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.warn(`Cloud contribute failed: err`);
return null;
}
}
async reportFailure(req) {
try {
await (0, undici_1.request)(`this.endpoint/v1/lobsters/report_failure`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 3000,
bodyTimeout: 3000,
});
}
catch (err) {
this.logger.debug(`Cloud report_failure failed: err`);
}
}
}
exports.CloudClient = CloudClient;
FILE:dist/index.d.ts
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
export { interceptIntent, onSessionComplete } from "./interceptor.js";
FILE:dist/index.js
"use strict";
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.onSessionComplete = exports.interceptIntent = void 0;
var interceptor_js_1 = require("./interceptor.js");
Object.defineProperty(exports, "interceptIntent", { enumerable: true, get: function () { return interceptor_js_1.interceptIntent; } });
Object.defineProperty(exports, "onSessionComplete", { enumerable: true, get: function () { return interceptor_js_1.onSessionComplete; } });
FILE:dist/intent-parser.d.ts
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
export interface ParsedIntent {
raw: string;
normalized: string;
domain: string;
action_verb: string;
object_noun: string;
}
export declare function parseIntent(raw: string, url: string): ParsedIntent;
FILE:dist/intent-parser.js
"use strict";
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseIntent = parseIntent;
const ACTION_VERBS = new Set([
"click", "type", "fill", "submit", "select", "check", "uncheck",
"toggle", "scroll", "navigate", "open", "close", "search",
"login", "logout", "sign_in", "sign_up", "register",
"download", "upload", "delete", "remove", "add", "create",
"edit", "update", "save", "cancel", "confirm", "accept",
"reject", "deny", "approve", "buy", "purchase", "order",
"subscribe", "unsubscribe", "bookmark", "share", "copy",
"paste", "drag", "drop", "hover", "wait", "refresh",
]);
function parseIntent(raw, url) {
const normalized = raw
.toLowerCase()
.trim()
.replace(/\s+/g, " ")
.replace(/[^\w\s]/g, "");
const words = normalized.split(" ");
// Extract action verb
let actionVerb = "interact";
for (const word of words) {
if (ACTION_VERBS.has(word)) {
actionVerb = word;
break;
}
}
// Extract object noun (first noun-like word after the verb)
const verbIndex = words.indexOf(actionVerb);
const objectWords = words.slice(verbIndex + 1).filter((w) => w.length > 2 && !["the", "a", "an", "in", "on", "at", "to", "for", "with"].includes(w));
const objectNoun = objectWords.join("_") || "element";
// Extract domain from URL
let domain = "unknown";
try {
domain = new URL(url).hostname.replace("www.", "");
}
catch {
// ignore
}
return {
raw,
normalized,
domain,
action_verb: actionVerb,
object_noun: objectNoun,
};
}
FILE:dist/interceptor.d.ts
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Interceptor queries ClawMind Cloud for a matching Lobster macro
* 3. If hit → execute the Lobster workflow directly (skip LLM exploration)
* 4. If miss → passthrough to normal OpenClaw flow
* 5. On success → contribute the trace back to the cloud
* 6. On failure → report failure for circuit breaker tracking
*/
import type { OpenClawContext } from "./types.js";
export declare function interceptIntent(ctx: OpenClawContext): Promise<void>;
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and contributes it.
*/
export declare function onSessionComplete(ctx: OpenClawContext): Promise<void>;
FILE:dist/interceptor.js
"use strict";
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Interceptor queries ClawMind Cloud for a matching Lobster macro
* 3. If hit → execute the Lobster workflow directly (skip LLM exploration)
* 4. If miss → passthrough to normal OpenClaw flow
* 5. On success → contribute the trace back to the cloud
* 6. On failure → report failure for circuit breaker tracking
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.interceptIntent = interceptIntent;
exports.onSessionComplete = onSessionComplete;
const cloud_client_js_1 = require("./cloud-client.js");
const intent_parser_js_1 = require("./intent-parser.js");
const trace_compiler_js_1 = require("./trace-compiler.js");
async function interceptIntent(ctx) {
const { browser, lobster, sessions, gateway, workspace, config, logger } = ctx;
// Check if skill is enabled
if (!config.get("enabled")) {
gateway.passthrough();
return;
}
const cloudEndpoint = config.get("cloud_endpoint");
const timeoutMs = config.get("timeout_ms");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, timeoutMs, logger);
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
const intent = history.intent;
if (!intent) {
gateway.passthrough();
return;
}
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
const parsed = (0, intent_parser_js_1.parseIntent)(intent, url);
logger.info(`[ClawMind] Intercepting intent: "parsed.normalized" on parsed.domain`);
// Query cloud for a matching macro
let matchResult = null;
try {
matchResult = await client.match({
intent: parsed.normalized,
url,
dom_skeleton_hash: domHash,
node_id: nodeId,
});
}
catch {
// Cloud unavailable — graceful degradation, just passthrough
logger.debug("[ClawMind] Cloud unreachable, passing through");
gateway.passthrough();
return;
}
// No match — let OpenClaw handle it normally
if (!matchResult || !matchResult.hit || !matchResult.macro) {
logger.info("[ClawMind] No macro match, passing through to OpenClaw");
gateway.passthrough();
return;
}
const macro = matchResult.macro;
logger.info(`[ClawMind] Match found: macro.macro_id (score: matchResult.match_score, method: matchResult.match_method)`);
// Validate the workflow before execution
if (!lobster.validate(macro.lobster_workflow)) {
logger.warn(`[ClawMind] Workflow validation failed for macro.macro_id`);
gateway.passthrough();
return;
}
// Execute the Lobster workflow
let execResult;
try {
execResult = await lobster.execute(macro.lobster_workflow);
}
catch (err) {
logger.error(`[ClawMind] Lobster execution threw: err`);
// Report failure to cloud (fire-and-forget)
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: "other",
error_detail: String(err),
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
return;
}
if (execResult.success) {
logger.info(`[ClawMind] Macro macro.macro_id executed successfully (execResult.steps_completed/execResult.total_steps steps)`);
gateway.respond(`✅ Done via ClawMind cached workflow (macro.macro_id). ` +
`execResult.steps_completed steps replayed.`);
}
else {
logger.warn(`[ClawMind] Macro macro.macro_id failed at step execResult.failed_step_id: execResult.error`);
// Map error to error_type
const errorType = mapErrorType(execResult.error || "");
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: errorType,
error_detail: execResult.error,
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
}
}
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and contributes it.
*/
async function onSessionComplete(ctx) {
const { browser, sessions, workspace, config, logger } = ctx;
if (!config.get("enabled") || !config.get("auto_contribute")) {
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
// Only contribute successful sessions with meaningful actions
if (history.status !== "success")
return;
if (history.actions.length < 2)
return;
const intent = history.intent;
if (!intent)
return;
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
// Compile the trace into a Lobster workflow
const { workflow, argCount } = (0, trace_compiler_js_1.compileTrace)(intent, history.actions);
// Skip trivial workflows (single step, no real logic)
if (workflow.steps.length < 2) {
logger.debug("[ClawMind] Workflow too trivial to contribute, skipping");
return;
}
logger.info(`[ClawMind] Contributing workflow "workflow.name" (workflow.steps.length steps, argCount args)`);
const cloudEndpoint = config.get("cloud_endpoint");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, 5000, logger);
try {
const result = await client.contribute({
node_id: nodeId,
intent,
url,
dom_skeleton_hash: domHash,
lobster_workflow: workflow,
session_id: sessionId,
});
if (result?.accepted) {
logger.info(`[ClawMind] Contribution accepted: result.macro_id`);
}
else {
logger.debug(`[ClawMind] Contribution not accepted: result?.reason`);
}
}
catch (err) {
logger.debug(`[ClawMind] Contribution failed: err`);
}
}
function mapErrorType(error) {
const lower = error.toLowerCase();
if (lower.includes("selector") || lower.includes("not found") || lower.includes("no element")) {
return "selector_not_found";
}
if (lower.includes("timeout") || lower.includes("timed out")) {
return "timeout";
}
if (lower.includes("unexpected") || lower.includes("state")) {
return "unexpected_state";
}
return "other";
}
FILE:dist/sanitizer.d.ts
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
interface SanitizeResult {
sanitized: string;
extractedArgs: Map<string, ArgDefinition>;
}
interface ArgDefinition {
type: "string" | "number" | "boolean";
placeholder: string;
originalPattern: string;
}
export declare function sanitizeTrace(raw: string): SanitizeResult;
export declare function sanitizeActionArgs(args: Record<string, unknown>): {
sanitized: Record<string, unknown>;
extractedArgs: Map<string, ArgDefinition>;
};
export {};
FILE:dist/sanitizer.js
"use strict";
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeTrace = sanitizeTrace;
exports.sanitizeActionArgs = sanitizeActionArgs;
const PII_RULES = [
{
name: "email",
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
argType: "string",
},
{
name: "phone",
pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
argType: "string",
},
{
name: "card",
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
argType: "string",
},
{
name: "ssn",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
argType: "string",
},
{
name: "password",
pattern: /(?:password|passwd|pwd)\s*[:=]\s*\S+/gi,
argType: "string",
},
{
name: "api_key",
pattern: /(?:api[_-]?key|token|secret)\s*[:=]\s*[A-Za-z0-9_\-]{16,}/gi,
argType: "string",
},
{
name: "ip_address",
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
argType: "string",
},
];
// Common user-input field names that likely contain personal data
const SENSITIVE_FIELD_NAMES = new Set([
"username",
"user_name",
"first_name",
"last_name",
"full_name",
"name",
"email",
"phone",
"address",
"street",
"city",
"zip",
"zipcode",
"postal_code",
"ssn",
"social_security",
"credit_card",
"card_number",
"cvv",
"expiry",
"password",
"passwd",
"secret",
"token",
"api_key",
"dob",
"date_of_birth",
"birthday",
]);
function sanitizeTrace(raw) {
let result = raw;
const extractedArgs = new Map();
let argCounter = 0;
// Pass 1: Regex-based PII detection
for (const rule of PII_RULES) {
const matches = result.match(rule.pattern);
if (matches) {
for (const match of new Set(matches)) {
const argName = `rule.name_argCounter++`;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
extractedArgs.set(argName, {
type: rule.argType,
placeholder,
originalPattern: rule.name,
});
result = result.replaceAll(match, placeholder);
}
}
rule.pattern.lastIndex = 0;
}
return { sanitized: result, extractedArgs };
}
function sanitizeActionArgs(args) {
const allExtracted = new Map();
const sanitized = {};
for (const [key, value] of Object.entries(args)) {
const lowerKey = key.toLowerCase();
// If the field name itself is sensitive, replace the value entirely
if (SENSITIVE_FIELD_NAMES.has(lowerKey)) {
const argName = lowerKey;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
allExtracted.set(argName, {
type: typeof value === "number" ? "number" : "string",
placeholder,
originalPattern: `field:key`,
});
sanitized[key] = placeholder;
continue;
}
// If the value is a string, run PII regex sanitization
if (typeof value === "string") {
const { sanitized: cleanValue, extractedArgs } = sanitizeTrace(value);
sanitized[key] = cleanValue;
for (const [k, v] of extractedArgs) {
allExtracted.set(k, v);
}
}
else {
sanitized[key] = value;
}
}
return { sanitized, extractedArgs: allExtracted };
}
FILE:dist/trace-compiler.d.ts
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
import type { ActionTrace, LobsterWorkflow } from "./types.js";
export interface CompileResult {
workflow: LobsterWorkflow;
argCount: number;
}
export declare function compileTrace(intentName: string, actions: ActionTrace[]): CompileResult;
FILE:dist/trace-compiler.js
"use strict";
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileTrace = compileTrace;
const sanitizer_js_1 = require("./sanitizer.js");
function compileTrace(intentName, actions) {
const steps = [];
const collectedArgs = {};
let stepIndex = 0;
for (const action of actions) {
// Skip failed actions — we only want the successful path
if (!action.result.success)
continue;
// Skip non-browser actions (internal bookkeeping, etc.)
if (!isBrowserAction(action))
continue;
// Sanitize args to strip PII and extract Lobster arguments
const { sanitized, extractedArgs } = (0, sanitizer_js_1.sanitizeActionArgs)(action.args);
// Register extracted args in the workflow args definition
for (const [argName, argDef] of extractedArgs) {
if (!collectedArgs[argName]) {
collectedArgs[argName] = {
type: argDef.type,
required: true,
};
}
}
// Build the Lobster command string
const command = buildCommand(action.tool, action.action, sanitized);
const step = {
id: `step_stepIndex++`,
command,
timeout_ms: inferTimeout(action),
};
// Merge consecutive waits
if (steps.length > 0 && isWaitStep(step) && isWaitStep(steps[steps.length - 1])) {
const prev = steps[steps.length - 1];
prev.timeout_ms = (prev.timeout_ms || 1000) + (step.timeout_ms || 1000);
continue;
}
steps.push(step);
}
// Optimize: remove trailing waits
while (steps.length > 0 && isWaitStep(steps[steps.length - 1])) {
steps.pop();
}
const workflow = {
name: intentToWorkflowName(intentName),
args: collectedArgs,
steps,
};
return { workflow, argCount: Object.keys(collectedArgs).length };
}
function isBrowserAction(action) {
const browserTools = [
"browser.click",
"browser.type",
"browser.fill",
"browser.select",
"browser.check",
"browser.navigate",
"browser.scroll",
"browser.wait",
"browser.hover",
"browser.press",
"browser.evaluate",
];
const fullAction = `action.tool.action.action`;
return browserTools.some((bt) => fullAction.startsWith(bt));
}
function buildCommand(tool, action, args) {
const argsStr = Object.entries(args)
.map(([k, v]) => {
if (typeof v === "string")
return `k="v"`;
return `k=v`;
})
.join(" ");
return `openclaw.invoke tool.action argsStr`.trim();
}
function inferTimeout(action) {
const actionName = `action.tool.action.action`;
if (actionName.includes("navigate"))
return 10000;
if (actionName.includes("wait"))
return 5000;
if (actionName.includes("click"))
return 3000;
if (actionName.includes("type") || actionName.includes("fill"))
return 2000;
return 3000;
}
function isWaitStep(step) {
return step.command.includes("browser.wait");
}
function intentToWorkflowName(intent) {
return intent
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.trim()
.replace(/\s+/g, "_")
.slice(0, 64);
}
FILE:dist/types.d.ts
export interface OpenClawContext {
workspace: WorkspaceAPI;
browser: BrowserAPI;
lobster: LobsterAPI;
sessions: SessionsAPI;
gateway: GatewayAPI;
config: SkillConfig;
logger: Logger;
}
export interface WorkspaceAPI {
getNodeId(): string;
}
export interface BrowserAPI {
getCurrentUrl(): Promise<string>;
getDomSkeletonHash(): Promise<string>;
invoke(action: string, args: Record<string, unknown>): Promise<BrowserResult>;
}
export interface BrowserResult {
success: boolean;
error?: string;
data?: unknown;
}
export interface LobsterAPI {
execute(workflow: LobsterWorkflow): Promise<LobsterExecutionResult>;
validate(workflow: LobsterWorkflow): boolean;
}
export interface LobsterExecutionResult {
success: boolean;
steps_completed: number;
total_steps: number;
error?: string;
failed_step_id?: string;
}
export interface SessionsAPI {
getHistory(sessionId: string): Promise<SessionHistory>;
getCurrentSessionId(): string;
}
export interface SessionHistory {
session_id: string;
intent: string;
actions: ActionTrace[];
status: "success" | "failure" | "in_progress";
started_at: string;
completed_at?: string;
}
export interface ActionTrace {
tool: string;
action: string;
args: Record<string, unknown>;
result: {
success: boolean;
error?: string;
};
timestamp: string;
}
export interface GatewayAPI {
passthrough(): void;
respond(message: string): void;
}
export interface SkillConfig {
get<T>(key: string): T;
}
export interface Logger {
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
}
export interface LobsterStep {
id: string;
command: string;
env?: Record<string, string>;
timeout_ms?: number;
}
export interface LobsterArgs {
[key: string]: {
type: "string" | "number" | "boolean";
default?: string | number | boolean;
required?: boolean;
};
}
export interface LobsterWorkflow {
name: string;
args: LobsterArgs;
steps: LobsterStep[];
}
export interface MatchRequest {
intent: string;
intent_embedding?: number[];
url: string;
dom_skeleton_hash?: string;
node_id: string;
}
export interface MatchResponse {
hit: boolean;
macro?: {
macro_id: string;
trigger_context: {
intent_embedding: number[];
url_regex: string;
dom_skeleton_hash?: string;
};
lobster_workflow: LobsterWorkflow;
metadata: {
author_node: string;
success_rate: number;
success_count: number;
failure_count: number;
last_verified: string;
created_at: string;
};
status: string;
};
match_score?: number;
match_method?: string;
}
export interface ContributeRequest {
node_id: string;
intent: string;
url: string;
dom_skeleton_hash?: string;
lobster_workflow: LobsterWorkflow;
intent_embedding?: number[];
session_id: string;
}
export interface ContributeResponse {
accepted: boolean;
macro_id?: string;
reason?: string;
}
export interface ReportFailureRequest {
macro_id: string;
node_id: string;
error_type: "selector_not_found" | "timeout" | "unexpected_state" | "other";
error_detail?: string;
}
FILE:dist/types.js
"use strict";
// ===== OpenClaw SDK Types (Skill-side interfaces) =====
Object.defineProperty(exports, "__esModule", { value: true });
FILE:package-lock.json
{
"name": "clawmind-skill",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawmind-skill",
"version": "1.0.0",
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}
FILE:package.json
{
"name": "workflow-cache",
"version": "1.0.3",
"displayName": "Workflow Cache",
"description": "Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost.",
"author": "ainclaw",
"homepage": "https://clawhub.ai/workflow-cache",
"license": "MIT-0",
"keywords": ["ai-efficiency", "token-saver", "automation", "crowdsourced", "workflow-cache"],
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"typescript": "^5.7.0",
"@types/node": "^22.0.0"
}
}
FILE:README.md
# Workflow Cache
**One agent explores, all agents benefit.**
A crowdsourced Lobster workflow registry that caches successful automation patterns.
## Features
- **Interceptor**: Query cloud for cached workflows before LLM exploration
- **Trace Compiler**: Convert session traces to reusable Lobster workflows
- **PII Sanitizer**: Local-first privacy protection
## Hooks
- `on_intent_received` → Query cloud, replay workflow if hit
- `on_session_complete` → Compile and contribute successful sessions
## Quick Start
```bash
cd skill
npm install
npm run build
```
## Configuration
See `skill.json` for configurable options.
## License
MIT-0 — Free to use, modify, and redistribute.
FILE:skill.json
{
"name": "workflow-cache",
"version": "1.0.3",
"display_name": "Workflow Cache",
"description": "Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost.",
"author": "ainclaw",
"homepage": "https://clawhub.ai/workflow-cache",
"license": "MIT-0",
"tags": ["ai-efficiency", "token-saver", "automation", "crowdsourced", "workflow-cache"],
"entry": "dist/index.js",
"permissions": [
"browser",
"lobster",
"sessions_history",
"network"
],
"hooks": {
"on_intent_received": "interceptIntent",
"on_session_complete": "onSessionComplete"
},
"config": {
"cloud_endpoint": {
"type": "string",
"default": "https://api.workflowcache.dev",
"description": "Cloud API endpoint for workflow cache"
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable/disable workflow cache interception"
},
"auto_contribute": {
"type": "boolean",
"default": true,
"description": "Automatically contribute successful workflows"
},
"timeout_ms": {
"type": "number",
"default": 300,
"description": "Cloud API timeout in milliseconds"
}
}
}Cloud workflow cache for OpenClaw. Reduces token usage by reusing verified automation patterns.
---
name: clawmind
description: "Cloud workflow cache for OpenClaw. Reduces token usage by reusing verified automation patterns."
metadata:
openclaw:
emoji: "🧠"
---
# ClawMind
Cloud workflow registry for OpenClaw agents.
## What It Does
Caches successful automation workflows so agents can reuse them instead of regenerating from scratch.
**Benefits:**
- Lower token usage (up to 80% reduction)
- Faster execution (cached workflows run instantly)
- Auto-updating (workflows refresh when websites change)
## How It Works
1. Intercepts user intent before LLM processing
2. Queries cloud for matching cached workflow
3. If found: executes directly
4. If not found: normal LLM flow, then contributes successful result
## Configuration
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `cloud_endpoint` | string | `https://api.clawmind.dev` | Cloud API endpoint |
| `enabled` | boolean | `true` | Enable/disable interception |
| `auto_contribute` | boolean | `true` | Auto-contribute successful workflows |
| `timeout_ms` | number | `300` | API timeout (ms) |
## Installation
```bash
npx clawhub install ainclaw-cloudmind
```
## Privacy
- All PII stays local
- Only workflow patterns are shared
- Full sanitization before upload
## License
MIT-0
FILE:dist/cloud-client.d.ts
import type { MatchRequest, MatchResponse, ContributeRequest, ContributeResponse, ReportFailureRequest, Logger } from "./types.js";
export declare class CloudClient {
private endpoint;
private timeoutMs;
private logger;
constructor(endpoint: string, timeoutMs: number, logger: Logger);
match(req: MatchRequest): Promise<MatchResponse | null>;
contribute(req: ContributeRequest): Promise<ContributeResponse | null>;
reportFailure(req: ReportFailureRequest): Promise<void>;
}
FILE:dist/cloud-client.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudClient = void 0;
const undici_1 = require("undici");
class CloudClient {
endpoint;
timeoutMs;
logger;
constructor(endpoint, timeoutMs, logger) {
this.endpoint = endpoint.replace(/\/$/, "");
this.timeoutMs = timeoutMs;
this.logger = logger;
}
async match(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/match`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: this.timeoutMs,
bodyTimeout: this.timeoutMs,
});
if (statusCode !== 200) {
this.logger.warn(`Cloud match returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.debug(`Cloud match failed (expected on timeout/offline): err`);
return null;
}
}
async contribute(req) {
try {
const { statusCode, body } = await (0, undici_1.request)(`this.endpoint/v1/lobsters/contribute`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 5000,
bodyTimeout: 5000,
});
if (statusCode !== 201 && statusCode !== 409) {
this.logger.warn(`Cloud contribute returned statusCode`);
return null;
}
return (await body.json());
}
catch (err) {
this.logger.warn(`Cloud contribute failed: err`);
return null;
}
}
async reportFailure(req) {
try {
await (0, undici_1.request)(`this.endpoint/v1/lobsters/report_failure`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(req),
headersTimeout: 3000,
bodyTimeout: 3000,
});
}
catch (err) {
this.logger.debug(`Cloud report_failure failed: err`);
}
}
}
exports.CloudClient = CloudClient;
FILE:dist/index.d.ts
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
export { interceptIntent, onSessionComplete } from "./interceptor.js";
FILE:dist/index.js
"use strict";
/**
* ClawMind Skill Entry Point
*
* Exports the two hooks registered in skill.json:
* - interceptIntent: fires on every user intent, queries cloud for cached macros
* - onSessionComplete: fires after successful sessions, contributes new workflows
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.onSessionComplete = exports.interceptIntent = void 0;
var interceptor_js_1 = require("./interceptor.js");
Object.defineProperty(exports, "interceptIntent", { enumerable: true, get: function () { return interceptor_js_1.interceptIntent; } });
Object.defineProperty(exports, "onSessionComplete", { enumerable: true, get: function () { return interceptor_js_1.onSessionComplete; } });
FILE:dist/intent-parser.d.ts
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
export interface ParsedIntent {
raw: string;
normalized: string;
domain: string;
action_verb: string;
object_noun: string;
}
export declare function parseIntent(raw: string, url: string): ParsedIntent;
FILE:dist/intent-parser.js
"use strict";
/**
* Intent parser — extracts structured intent from natural language
* and current browser context. Lightweight local heuristics,
* no LLM call needed for basic intent normalization.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseIntent = parseIntent;
const ACTION_VERBS = new Set([
"click", "type", "fill", "submit", "select", "check", "uncheck",
"toggle", "scroll", "navigate", "open", "close", "search",
"login", "logout", "sign_in", "sign_up", "register",
"download", "upload", "delete", "remove", "add", "create",
"edit", "update", "save", "cancel", "confirm", "accept",
"reject", "deny", "approve", "buy", "purchase", "order",
"subscribe", "unsubscribe", "bookmark", "share", "copy",
"paste", "drag", "drop", "hover", "wait", "refresh",
]);
function parseIntent(raw, url) {
const normalized = raw
.toLowerCase()
.trim()
.replace(/\s+/g, " ")
.replace(/[^\w\s]/g, "");
const words = normalized.split(" ");
// Extract action verb
let actionVerb = "interact";
for (const word of words) {
if (ACTION_VERBS.has(word)) {
actionVerb = word;
break;
}
}
// Extract object noun (first noun-like word after the verb)
const verbIndex = words.indexOf(actionVerb);
const objectWords = words.slice(verbIndex + 1).filter((w) => w.length > 2 && !["the", "a", "an", "in", "on", "at", "to", "for", "with"].includes(w));
const objectNoun = objectWords.join("_") || "element";
// Extract domain from URL
let domain = "unknown";
try {
domain = new URL(url).hostname.replace("www.", "");
}
catch {
// ignore
}
return {
raw,
normalized,
domain,
action_verb: actionVerb,
object_noun: objectNoun,
};
}
FILE:dist/interceptor.d.ts
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Interceptor queries ClawMind Cloud for a matching Lobster macro
* 3. If hit → execute the Lobster workflow directly (skip LLM exploration)
* 4. If miss → passthrough to normal OpenClaw flow
* 5. On success → contribute the trace back to the cloud
* 6. On failure → report failure for circuit breaker tracking
*/
import type { OpenClawContext } from "./types.js";
export declare function interceptIntent(ctx: OpenClawContext): Promise<void>;
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and contributes it.
*/
export declare function onSessionComplete(ctx: OpenClawContext): Promise<void>;
FILE:dist/interceptor.js
"use strict";
/**
* Interceptor — the main hook that fires on every intent.
*
* Flow:
* 1. User issues an intent (e.g., "login to GitHub")
* 2. Interceptor queries ClawMind Cloud for a matching Lobster macro
* 3. If hit → execute the Lobster workflow directly (skip LLM exploration)
* 4. If miss → passthrough to normal OpenClaw flow
* 5. On success → contribute the trace back to the cloud
* 6. On failure → report failure for circuit breaker tracking
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.interceptIntent = interceptIntent;
exports.onSessionComplete = onSessionComplete;
const cloud_client_js_1 = require("./cloud-client.js");
const intent_parser_js_1 = require("./intent-parser.js");
const trace_compiler_js_1 = require("./trace-compiler.js");
async function interceptIntent(ctx) {
const { browser, lobster, sessions, gateway, workspace, config, logger } = ctx;
// Check if skill is enabled
if (!config.get("enabled")) {
gateway.passthrough();
return;
}
const cloudEndpoint = config.get("cloud_endpoint");
const timeoutMs = config.get("timeout_ms");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, timeoutMs, logger);
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
const intent = history.intent;
if (!intent) {
gateway.passthrough();
return;
}
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
const parsed = (0, intent_parser_js_1.parseIntent)(intent, url);
logger.info(`[ClawMind] Intercepting intent: "parsed.normalized" on parsed.domain`);
// Query cloud for a matching macro
let matchResult = null;
try {
matchResult = await client.match({
intent: parsed.normalized,
url,
dom_skeleton_hash: domHash,
node_id: nodeId,
});
}
catch {
// Cloud unavailable — graceful degradation, just passthrough
logger.debug("[ClawMind] Cloud unreachable, passing through");
gateway.passthrough();
return;
}
// No match — let OpenClaw handle it normally
if (!matchResult || !matchResult.hit || !matchResult.macro) {
logger.info("[ClawMind] No macro match, passing through to OpenClaw");
gateway.passthrough();
return;
}
const macro = matchResult.macro;
logger.info(`[ClawMind] Match found: macro.macro_id (score: matchResult.match_score, method: matchResult.match_method)`);
// Validate the workflow before execution
if (!lobster.validate(macro.lobster_workflow)) {
logger.warn(`[ClawMind] Workflow validation failed for macro.macro_id`);
gateway.passthrough();
return;
}
// Execute the Lobster workflow
let execResult;
try {
execResult = await lobster.execute(macro.lobster_workflow);
}
catch (err) {
logger.error(`[ClawMind] Lobster execution threw: err`);
// Report failure to cloud (fire-and-forget)
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: "other",
error_detail: String(err),
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
return;
}
if (execResult.success) {
logger.info(`[ClawMind] Macro macro.macro_id executed successfully (execResult.steps_completed/execResult.total_steps steps)`);
gateway.respond(`✅ Done via ClawMind cached workflow (macro.macro_id). ` +
`execResult.steps_completed steps replayed.`);
}
else {
logger.warn(`[ClawMind] Macro macro.macro_id failed at step execResult.failed_step_id: execResult.error`);
// Map error to error_type
const errorType = mapErrorType(execResult.error || "");
client.reportFailure({
macro_id: macro.macro_id,
node_id: nodeId,
error_type: errorType,
error_detail: execResult.error,
}).catch(() => { });
// Fall back to normal OpenClaw flow
gateway.passthrough();
}
}
/**
* Hook: called when a session completes successfully.
* Compiles the session trace into a Lobster workflow and contributes it.
*/
async function onSessionComplete(ctx) {
const { browser, sessions, workspace, config, logger } = ctx;
if (!config.get("enabled") || !config.get("auto_contribute")) {
return;
}
const sessionId = sessions.getCurrentSessionId();
const history = await sessions.getHistory(sessionId);
// Only contribute successful sessions with meaningful actions
if (history.status !== "success")
return;
if (history.actions.length < 2)
return;
const intent = history.intent;
if (!intent)
return;
const url = await browser.getCurrentUrl();
const domHash = await browser.getDomSkeletonHash();
const nodeId = workspace.getNodeId();
// Compile the trace into a Lobster workflow
const { workflow, argCount } = (0, trace_compiler_js_1.compileTrace)(intent, history.actions);
// Skip trivial workflows (single step, no real logic)
if (workflow.steps.length < 2) {
logger.debug("[ClawMind] Workflow too trivial to contribute, skipping");
return;
}
logger.info(`[ClawMind] Contributing workflow "workflow.name" (workflow.steps.length steps, argCount args)`);
const cloudEndpoint = config.get("cloud_endpoint");
const client = new cloud_client_js_1.CloudClient(cloudEndpoint, 5000, logger);
try {
const result = await client.contribute({
node_id: nodeId,
intent,
url,
dom_skeleton_hash: domHash,
lobster_workflow: workflow,
session_id: sessionId,
});
if (result?.accepted) {
logger.info(`[ClawMind] Contribution accepted: result.macro_id`);
}
else {
logger.debug(`[ClawMind] Contribution not accepted: result?.reason`);
}
}
catch (err) {
logger.debug(`[ClawMind] Contribution failed: err`);
}
}
function mapErrorType(error) {
const lower = error.toLowerCase();
if (lower.includes("selector") || lower.includes("not found") || lower.includes("no element")) {
return "selector_not_found";
}
if (lower.includes("timeout") || lower.includes("timed out")) {
return "timeout";
}
if (lower.includes("unexpected") || lower.includes("state")) {
return "unexpected_state";
}
return "other";
}
FILE:dist/sanitizer.d.ts
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
interface SanitizeResult {
sanitized: string;
extractedArgs: Map<string, ArgDefinition>;
}
interface ArgDefinition {
type: "string" | "number" | "boolean";
placeholder: string;
originalPattern: string;
}
export declare function sanitizeTrace(raw: string): SanitizeResult;
export declare function sanitizeActionArgs(args: Record<string, unknown>): {
sanitized: Record<string, unknown>;
extractedArgs: Map<string, ArgDefinition>;
};
export {};
FILE:dist/sanitizer.js
"use strict";
/**
* Local PII sanitizer — the primary privacy gate.
* Runs BEFORE any data leaves the node.
* Strips all identifiable information from action traces
* and replaces them with Lobster argument placeholders.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeTrace = sanitizeTrace;
exports.sanitizeActionArgs = sanitizeActionArgs;
const PII_RULES = [
{
name: "email",
pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
argType: "string",
},
{
name: "phone",
pattern: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
argType: "string",
},
{
name: "card",
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
argType: "string",
},
{
name: "ssn",
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
argType: "string",
},
{
name: "password",
pattern: /(?:password|passwd|pwd)\s*[:=]\s*\S+/gi,
argType: "string",
},
{
name: "api_key",
pattern: /(?:api[_-]?key|token|secret)\s*[:=]\s*[A-Za-z0-9_\-]{16,}/gi,
argType: "string",
},
{
name: "ip_address",
pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
argType: "string",
},
];
// Common user-input field names that likely contain personal data
const SENSITIVE_FIELD_NAMES = new Set([
"username",
"user_name",
"first_name",
"last_name",
"full_name",
"name",
"email",
"phone",
"address",
"street",
"city",
"zip",
"zipcode",
"postal_code",
"ssn",
"social_security",
"credit_card",
"card_number",
"cvv",
"expiry",
"password",
"passwd",
"secret",
"token",
"api_key",
"dob",
"date_of_birth",
"birthday",
]);
function sanitizeTrace(raw) {
let result = raw;
const extractedArgs = new Map();
let argCounter = 0;
// Pass 1: Regex-based PII detection
for (const rule of PII_RULES) {
const matches = result.match(rule.pattern);
if (matches) {
for (const match of new Set(matches)) {
const argName = `rule.name_argCounter++`;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
extractedArgs.set(argName, {
type: rule.argType,
placeholder,
originalPattern: rule.name,
});
result = result.replaceAll(match, placeholder);
}
}
rule.pattern.lastIndex = 0;
}
return { sanitized: result, extractedArgs };
}
function sanitizeActionArgs(args) {
const allExtracted = new Map();
const sanitized = {};
for (const [key, value] of Object.entries(args)) {
const lowerKey = key.toLowerCase();
// If the field name itself is sensitive, replace the value entirely
if (SENSITIVE_FIELD_NAMES.has(lowerKey)) {
const argName = lowerKey;
const placeholder = `$LOBSTER_ARG_argName.toUpperCase()`;
allExtracted.set(argName, {
type: typeof value === "number" ? "number" : "string",
placeholder,
originalPattern: `field:key`,
});
sanitized[key] = placeholder;
continue;
}
// If the value is a string, run PII regex sanitization
if (typeof value === "string") {
const { sanitized: cleanValue, extractedArgs } = sanitizeTrace(value);
sanitized[key] = cleanValue;
for (const [k, v] of extractedArgs) {
allExtracted.set(k, v);
}
}
else {
sanitized[key] = value;
}
}
return { sanitized, extractedArgs: allExtracted };
}
FILE:dist/trace-compiler.d.ts
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
import type { ActionTrace, LobsterWorkflow } from "./types.js";
export interface CompileResult {
workflow: LobsterWorkflow;
argCount: number;
}
export declare function compileTrace(intentName: string, actions: ActionTrace[]): CompileResult;
FILE:dist/trace-compiler.js
"use strict";
/**
* Trace Compiler — converts a recorded OpenClaw session trace
* into a reusable Lobster workflow.
*
* This is the core "one node explores, all nodes benefit" engine.
* It takes the raw action history from a successful session and
* compiles it into a parameterized, portable Lobster workflow.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.compileTrace = compileTrace;
const sanitizer_js_1 = require("./sanitizer.js");
function compileTrace(intentName, actions) {
const steps = [];
const collectedArgs = {};
let stepIndex = 0;
for (const action of actions) {
// Skip failed actions — we only want the successful path
if (!action.result.success)
continue;
// Skip non-browser actions (internal bookkeeping, etc.)
if (!isBrowserAction(action))
continue;
// Sanitize args to strip PII and extract Lobster arguments
const { sanitized, extractedArgs } = (0, sanitizer_js_1.sanitizeActionArgs)(action.args);
// Register extracted args in the workflow args definition
for (const [argName, argDef] of extractedArgs) {
if (!collectedArgs[argName]) {
collectedArgs[argName] = {
type: argDef.type,
required: true,
};
}
}
// Build the Lobster command string
const command = buildCommand(action.tool, action.action, sanitized);
const step = {
id: `step_stepIndex++`,
command,
timeout_ms: inferTimeout(action),
};
// Merge consecutive waits
if (steps.length > 0 && isWaitStep(step) && isWaitStep(steps[steps.length - 1])) {
const prev = steps[steps.length - 1];
prev.timeout_ms = (prev.timeout_ms || 1000) + (step.timeout_ms || 1000);
continue;
}
steps.push(step);
}
// Optimize: remove trailing waits
while (steps.length > 0 && isWaitStep(steps[steps.length - 1])) {
steps.pop();
}
const workflow = {
name: intentToWorkflowName(intentName),
args: collectedArgs,
steps,
};
return { workflow, argCount: Object.keys(collectedArgs).length };
}
function isBrowserAction(action) {
const browserTools = [
"browser.click",
"browser.type",
"browser.fill",
"browser.select",
"browser.check",
"browser.navigate",
"browser.scroll",
"browser.wait",
"browser.hover",
"browser.press",
"browser.evaluate",
];
const fullAction = `action.tool.action.action`;
return browserTools.some((bt) => fullAction.startsWith(bt));
}
function buildCommand(tool, action, args) {
const argsStr = Object.entries(args)
.map(([k, v]) => {
if (typeof v === "string")
return `k="v"`;
return `k=v`;
})
.join(" ");
return `openclaw.invoke tool.action argsStr`.trim();
}
function inferTimeout(action) {
const actionName = `action.tool.action.action`;
if (actionName.includes("navigate"))
return 10000;
if (actionName.includes("wait"))
return 5000;
if (actionName.includes("click"))
return 3000;
if (actionName.includes("type") || actionName.includes("fill"))
return 2000;
return 3000;
}
function isWaitStep(step) {
return step.command.includes("browser.wait");
}
function intentToWorkflowName(intent) {
return intent
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.trim()
.replace(/\s+/g, "_")
.slice(0, 64);
}
FILE:dist/types.d.ts
export interface OpenClawContext {
workspace: WorkspaceAPI;
browser: BrowserAPI;
lobster: LobsterAPI;
sessions: SessionsAPI;
gateway: GatewayAPI;
config: SkillConfig;
logger: Logger;
}
export interface WorkspaceAPI {
getNodeId(): string;
}
export interface BrowserAPI {
getCurrentUrl(): Promise<string>;
getDomSkeletonHash(): Promise<string>;
invoke(action: string, args: Record<string, unknown>): Promise<BrowserResult>;
}
export interface BrowserResult {
success: boolean;
error?: string;
data?: unknown;
}
export interface LobsterAPI {
execute(workflow: LobsterWorkflow): Promise<LobsterExecutionResult>;
validate(workflow: LobsterWorkflow): boolean;
}
export interface LobsterExecutionResult {
success: boolean;
steps_completed: number;
total_steps: number;
error?: string;
failed_step_id?: string;
}
export interface SessionsAPI {
getHistory(sessionId: string): Promise<SessionHistory>;
getCurrentSessionId(): string;
}
export interface SessionHistory {
session_id: string;
intent: string;
actions: ActionTrace[];
status: "success" | "failure" | "in_progress";
started_at: string;
completed_at?: string;
}
export interface ActionTrace {
tool: string;
action: string;
args: Record<string, unknown>;
result: {
success: boolean;
error?: string;
};
timestamp: string;
}
export interface GatewayAPI {
passthrough(): void;
respond(message: string): void;
}
export interface SkillConfig {
get<T>(key: string): T;
}
export interface Logger {
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
}
export interface LobsterStep {
id: string;
command: string;
env?: Record<string, string>;
timeout_ms?: number;
}
export interface LobsterArgs {
[key: string]: {
type: "string" | "number" | "boolean";
default?: string | number | boolean;
required?: boolean;
};
}
export interface LobsterWorkflow {
name: string;
args: LobsterArgs;
steps: LobsterStep[];
}
export interface MatchRequest {
intent: string;
intent_embedding?: number[];
url: string;
dom_skeleton_hash?: string;
node_id: string;
}
export interface MatchResponse {
hit: boolean;
macro?: {
macro_id: string;
trigger_context: {
intent_embedding: number[];
url_regex: string;
dom_skeleton_hash?: string;
};
lobster_workflow: LobsterWorkflow;
metadata: {
author_node: string;
success_rate: number;
success_count: number;
failure_count: number;
last_verified: string;
created_at: string;
};
status: string;
};
match_score?: number;
match_method?: string;
}
export interface ContributeRequest {
node_id: string;
intent: string;
url: string;
dom_skeleton_hash?: string;
lobster_workflow: LobsterWorkflow;
intent_embedding?: number[];
session_id: string;
}
export interface ContributeResponse {
accepted: boolean;
macro_id?: string;
reason?: string;
}
export interface ReportFailureRequest {
macro_id: string;
node_id: string;
error_type: "selector_not_found" | "timeout" | "unexpected_state" | "other";
error_detail?: string;
}
FILE:dist/types.js
"use strict";
// ===== OpenClaw SDK Types (Skill-side interfaces) =====
Object.defineProperty(exports, "__esModule", { value: true });
FILE:package-lock.json
{
"name": "clawmind-skill",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawmind-skill",
"version": "1.0.0",
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}
FILE:package.json
{
"name": "clawmind",
"version": "1.0.5",
"displayName": "ClawMind",
"description": "Save up to 90% on Token costs. One agent explores, all agents benefit. Cloud-cached workflows with zero inference cost.",
"author": "ainclaw",
"homepage": "https://clawhub.ai/ainclaw-mind",
"license": "MIT-0",
"keywords": ["ai-efficiency", "token-saver", "automation", "crowdsourced", "workflow-cache"],
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"undici": "^7.2.0"
},
"devDependencies": {
"typescript": "^5.7.0",
"@types/node": "^22.0.0"
}
}
FILE:README.md
# Workflow Cache
**One agent explores, all agents benefit.**
A crowdsourced Lobster workflow registry that caches successful automation patterns.
## Features
- **Interceptor**: Query cloud for cached workflows before LLM exploration
- **Trace Compiler**: Convert session traces to reusable Lobster workflows
- **PII Sanitizer**: Local-first privacy protection
## Hooks
- `on_intent_received` → Query cloud, replay workflow if hit
- `on_session_complete` → Compile and contribute successful sessions
## Quick Start
```bash
cd skill
npm install
npm run build
```
## Configuration
See `skill.json` for configurable options.
## License
MIT-0 — Free to use, modify, and redistribute.
FILE:skill.json
{
"name": "workflow-cache",
"version": "1.0.4",
"display_name": "Workflow Cache",
"description": "Cloud workflow cache for OpenClaw. Reduces token usage by reusing verified automation patterns.",
"author": "ainclaw",
"homepage": "https://clawhub.ai/ainclaw-cloudmind",
"license": "MIT-0",
"tags": ["ai-efficiency", "token-saver", "automation"],
"entry": "dist/index.js",
"permissions": [
"browser",
"sessions_history",
"network"
],
"hooks": {
"on_intent_received": "interceptIntent",
"on_session_complete": "onSessionComplete"
},
"config": {
"cloud_endpoint": {
"type": "string",
"default": "https://api.workflowcache.dev",
"description": "Cloud API endpoint"
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable/disable cache"
},
"auto_contribute": {
"type": "boolean",
"default": true,
"description": "Auto-contribute workflows"
},
"timeout_ms": {
"type": "number",
"default": 300,
"description": "API timeout (ms)"
}
}
}分布式 AI 工作节点技能 - 连接星联(Skynet)调度系统,自动接单与执行任务
---
name: eagle-claw
description: 分布式 AI 工作节点技能 - 连接星联(Skynet)调度系统,自动接单与执行任务
metadata: {"openclaw":{"emoji":"🦅","requires":{"env":["SKYNET_WS_URL"]}}}
---
# 🦅 鹰爪技能 (Eagle Claw)
你已接入星联 (Skynet) 分布式 AI 协作网络。你是一个工作节点,可以接收和执行来自星联的任务。
## 核心功能
- **自动接单**:连接星联后自动接收任务
- **任务执行**:利用 OpenClaw 工具执行搜索、编程等任务
- **积分奖励**:完成任务赚取星联积分
- **信誉系统**:高质量交付提升信誉分
## 可用工具
你可以通过对话调用以下工具:
| 工具名 | 功能 |
|--------|------|
| `eagle_claw_connect` | 启动鹰爪节点,连接星联 |
| `eagle_claw_status` | 查询节点状态 |
| `eagle_claw_execute` | 手动提交任务 |
| `eagle_claw_disconnect` | 断开连接 |
## 使用示例
**连接星联:**
```
请连接星联
```
**查询状态:**
```
查看鹰爪状态
```
**执行任务:**
```
帮我搜索最新AI新闻
```
## 配置
首次使用会自动生成 Ed25519 身份密钥。也可以在环境变量中配置:
- `SKYNET_WS_URL`:星联 WebSocket 地址
- `PRIVATE_KEY`:Ed25519 私钥(可选)
## 更多信息
- GitHub: https://github.com/ainclaw/ainclaw
- 星联: https://www.ainclaw.com