camofox-browser 2.1.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/README.md +304 -33
- package/dist/src/cli/commands/content.d.ts.map +1 -1
- package/dist/src/cli/commands/content.js +37 -0
- package/dist/src/cli/commands/content.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +21 -4
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/interaction.d.ts.map +1 -1
- package/dist/src/cli/commands/interaction.js +5 -14
- package/dist/src/cli/commands/interaction.js.map +1 -1
- package/dist/src/cli/commands/navigation.d.ts.map +1 -1
- package/dist/src/cli/commands/navigation.js +12 -6
- package/dist/src/cli/commands/navigation.js.map +1 -1
- package/dist/src/cli/commands/server.d.ts.map +1 -1
- package/dist/src/cli/commands/server.js +9 -3
- package/dist/src/cli/commands/server.js.map +1 -1
- package/dist/src/cli/commands/session.d.ts.map +1 -1
- package/dist/src/cli/commands/session.js +23 -5
- package/dist/src/cli/commands/session.js.map +1 -1
- package/dist/src/cli/server/manager.d.ts +1 -0
- package/dist/src/cli/server/manager.d.ts.map +1 -1
- package/dist/src/cli/server/manager.js +7 -12
- package/dist/src/cli/server/manager.js.map +1 -1
- package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
- package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
- package/dist/src/middleware/lifecycle-activity.js +21 -0
- package/dist/src/middleware/lifecycle-activity.js.map +1 -0
- package/dist/src/openapi/spec.d.ts +4 -0
- package/dist/src/openapi/spec.d.ts.map +1 -0
- package/dist/src/openapi/spec.js +730 -0
- package/dist/src/openapi/spec.js.map +1 -0
- package/dist/src/routes/core.d.ts.map +1 -1
- package/dist/src/routes/core.js +428 -53
- package/dist/src/routes/core.js.map +1 -1
- package/dist/src/routes/docs.d.ts +3 -0
- package/dist/src/routes/docs.d.ts.map +1 -0
- package/dist/src/routes/docs.js +23 -0
- package/dist/src/routes/docs.js.map +1 -0
- package/dist/src/routes/openclaw.d.ts.map +1 -1
- package/dist/src/routes/openclaw.js +244 -90
- package/dist/src/routes/openclaw.js.map +1 -1
- package/dist/src/server.js +55 -4
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/context-pool.d.ts +19 -3
- package/dist/src/services/context-pool.d.ts.map +1 -1
- package/dist/src/services/context-pool.js +248 -65
- package/dist/src/services/context-pool.js.map +1 -1
- package/dist/src/services/download.d.ts +2 -0
- package/dist/src/services/download.d.ts.map +1 -1
- package/dist/src/services/download.js +110 -80
- package/dist/src/services/download.js.map +1 -1
- package/dist/src/services/lifecycle-controller.d.ts +40 -0
- package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
- package/dist/src/services/lifecycle-controller.js +106 -0
- package/dist/src/services/lifecycle-controller.js.map +1 -0
- package/dist/src/services/resource-extractor.d.ts +1 -0
- package/dist/src/services/resource-extractor.d.ts.map +1 -1
- package/dist/src/services/resource-extractor.js +7 -0
- package/dist/src/services/resource-extractor.js.map +1 -1
- package/dist/src/services/session.d.ts +84 -2
- package/dist/src/services/session.d.ts.map +1 -1
- package/dist/src/services/session.js +349 -47
- package/dist/src/services/session.js.map +1 -1
- package/dist/src/services/structured-extractor.d.ts +39 -0
- package/dist/src/services/structured-extractor.d.ts.map +1 -0
- package/dist/src/services/structured-extractor.js +487 -0
- package/dist/src/services/structured-extractor.js.map +1 -0
- package/dist/src/services/tab.d.ts +30 -3
- package/dist/src/services/tab.d.ts.map +1 -1
- package/dist/src/services/tab.js +877 -124
- package/dist/src/services/tab.js.map +1 -1
- package/dist/src/services/tracing.d.ts +7 -0
- package/dist/src/services/tracing.d.ts.map +1 -1
- package/dist/src/services/tracing.js +162 -19
- package/dist/src/services/tracing.js.map +1 -1
- package/dist/src/services/vnc.d.ts.map +1 -1
- package/dist/src/services/vnc.js +5 -3
- package/dist/src/services/vnc.js.map +1 -1
- package/dist/src/services/youtube.js +1 -1
- package/dist/src/services/youtube.js.map +1 -1
- package/dist/src/types.d.ts +71 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/config.d.ts +79 -3
- package/dist/src/utils/config.d.ts.map +1 -1
- package/dist/src/utils/config.js +145 -3
- package/dist/src/utils/config.js.map +1 -1
- package/dist/src/utils/presets.d.ts.map +1 -1
- package/dist/src/utils/presets.js +3 -1
- package/dist/src/utils/presets.js.map +1 -1
- package/dist/src/utils/proxy-profiles.d.ts +18 -0
- package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
- package/dist/src/utils/proxy-profiles.js +197 -0
- package/dist/src/utils/proxy-profiles.js.map +1 -0
- package/dist/src/utils/sidecar-version.d.ts +12 -0
- package/dist/src/utils/sidecar-version.d.ts.map +1 -0
- package/dist/src/utils/sidecar-version.js +63 -0
- package/dist/src/utils/sidecar-version.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/openclaw.plugin.json +39 -0
- package/package.json +16 -4
- package/plugin.ts +949 -0
package/plugin.ts
ADDED
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camoufox Browser - OpenClaw Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides browser automation tools using the Camoufox anti-detection browser.
|
|
5
|
+
* Server auto-starts when plugin loads (configurable via autoStart: false).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ChildProcess } from "child_process";
|
|
9
|
+
import { join, dirname, resolve } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
|
|
13
|
+
import { loadConfig } from "./dist/src/utils/config.js";
|
|
14
|
+
import { launchServer } from "./dist/src/utils/launcher.js";
|
|
15
|
+
import { readCookieFile } from "./dist/src/utils/cookies.js";
|
|
16
|
+
|
|
17
|
+
// Get plugin directory - works in both ESM and CJS contexts
|
|
18
|
+
const getPluginDir = (): string => {
|
|
19
|
+
try {
|
|
20
|
+
// ESM context
|
|
21
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
} catch {
|
|
23
|
+
// CJS context
|
|
24
|
+
return __dirname;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
interface PluginConfig {
|
|
29
|
+
url?: string;
|
|
30
|
+
autoStart?: boolean;
|
|
31
|
+
port?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ToolResult {
|
|
35
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface HealthCheckResult {
|
|
39
|
+
status: "ok" | "warn" | "error";
|
|
40
|
+
message?: string;
|
|
41
|
+
details?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ServerHealth {
|
|
45
|
+
status?: "ok" | "degraded";
|
|
46
|
+
consecutiveFailures?: number;
|
|
47
|
+
activeOps?: number;
|
|
48
|
+
tabs?: number;
|
|
49
|
+
sessions?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface CliCommandBuilder {
|
|
53
|
+
description: (desc: string) => CliCommandBuilder;
|
|
54
|
+
option: (flags: string, desc: string, defaultValue?: string) => CliCommandBuilder;
|
|
55
|
+
argument: (name: string, desc: string) => CliCommandBuilder;
|
|
56
|
+
action: <TArgs extends unknown[]>(handler: (...args: TArgs) => void | Promise<void>) => CliCommandBuilder;
|
|
57
|
+
command: (name: string) => CliCommandBuilder;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface CliContext {
|
|
61
|
+
program: CliCommandBuilder;
|
|
62
|
+
config: PluginConfig;
|
|
63
|
+
logger: {
|
|
64
|
+
info: (msg: string) => void;
|
|
65
|
+
error: (msg: string) => void;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ToolContext {
|
|
70
|
+
sessionKey?: string;
|
|
71
|
+
agentId?: string;
|
|
72
|
+
workspaceDir?: string;
|
|
73
|
+
sandboxed?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type ToolDefinition = {
|
|
77
|
+
name: string;
|
|
78
|
+
description: string;
|
|
79
|
+
parameters: object;
|
|
80
|
+
execute: (id: string, params: Record<string, unknown>) => Promise<ToolResult>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type ToolFactory = (ctx: ToolContext) => ToolDefinition | ToolDefinition[] | null | undefined;
|
|
84
|
+
|
|
85
|
+
interface PluginApi {
|
|
86
|
+
registerTool: (
|
|
87
|
+
tool: ToolDefinition | ToolFactory,
|
|
88
|
+
options?: { optional?: boolean }
|
|
89
|
+
) => void;
|
|
90
|
+
registerCommand: (cmd: {
|
|
91
|
+
name: string;
|
|
92
|
+
description: string;
|
|
93
|
+
handler: (args: string[]) => Promise<void>;
|
|
94
|
+
}) => void;
|
|
95
|
+
registerCli?: (
|
|
96
|
+
registrar: (ctx: CliContext) => void | Promise<void>,
|
|
97
|
+
opts?: { commands?: string[] }
|
|
98
|
+
) => void;
|
|
99
|
+
registerRpc?: (
|
|
100
|
+
name: string,
|
|
101
|
+
handler: (params: Record<string, unknown>) => Promise<unknown>
|
|
102
|
+
) => void;
|
|
103
|
+
registerHealthCheck?: (
|
|
104
|
+
name: string,
|
|
105
|
+
check: () => Promise<HealthCheckResult>
|
|
106
|
+
) => void;
|
|
107
|
+
config: Record<string, unknown>;
|
|
108
|
+
pluginConfig?: PluginConfig;
|
|
109
|
+
log: {
|
|
110
|
+
info: (msg: string) => void;
|
|
111
|
+
error: (msg: string) => void;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let serverProcess: ChildProcess | null = null;
|
|
116
|
+
|
|
117
|
+
async function startServer(
|
|
118
|
+
pluginDir: string,
|
|
119
|
+
port: number,
|
|
120
|
+
log: PluginApi["log"]
|
|
121
|
+
): Promise<ChildProcess> {
|
|
122
|
+
const cfg = loadConfig();
|
|
123
|
+
const proc = launchServer({
|
|
124
|
+
pluginDir,
|
|
125
|
+
port,
|
|
126
|
+
env: cfg.serverEnv as Record<string, string | undefined>,
|
|
127
|
+
log,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
proc.on("error", (err: Error) => {
|
|
131
|
+
log?.error?.(`Server process error: ${err.message}`);
|
|
132
|
+
serverProcess = null;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
proc.on("exit", (code: number | null) => {
|
|
136
|
+
if (code !== 0 && code !== null) {
|
|
137
|
+
log?.error?.(`Server exited with code ${code}`);
|
|
138
|
+
}
|
|
139
|
+
serverProcess = null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Wait for server to be ready
|
|
143
|
+
const baseUrl = `http://localhost:${port}`;
|
|
144
|
+
for (let i = 0; i < 30; i++) {
|
|
145
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch(`${baseUrl}/health`);
|
|
148
|
+
if (res.ok) {
|
|
149
|
+
log.info(`Camoufox server ready on port ${port}`);
|
|
150
|
+
return proc;
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Server not ready yet
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw new Error("Server failed to start within 15 seconds");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function checkServerRunning(baseUrl: string): Promise<boolean> {
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(`${baseUrl}/health`);
|
|
162
|
+
return res.ok;
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function fetchApi(
|
|
169
|
+
baseUrl: string,
|
|
170
|
+
path: string,
|
|
171
|
+
options: RequestInit = {}
|
|
172
|
+
): Promise<unknown> {
|
|
173
|
+
const envCfg = loadConfig();
|
|
174
|
+
const url = `${baseUrl}${path}`;
|
|
175
|
+
const headers: Record<string, string> = {
|
|
176
|
+
"Content-Type": "application/json",
|
|
177
|
+
...(options.headers as Record<string, string>),
|
|
178
|
+
};
|
|
179
|
+
if (envCfg.apiKey) {
|
|
180
|
+
headers["Authorization"] = `Bearer ${envCfg.apiKey}`;
|
|
181
|
+
}
|
|
182
|
+
const res = await fetch(url, {
|
|
183
|
+
...options,
|
|
184
|
+
headers,
|
|
185
|
+
});
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
const text = await res.text();
|
|
188
|
+
throw new Error(`${res.status}: ${text}`);
|
|
189
|
+
}
|
|
190
|
+
return res.json();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function fetchHealth(baseUrl: string): Promise<{ ok: boolean; httpStatus: number; data: ServerHealth }> {
|
|
194
|
+
const res = await fetch(`${baseUrl}/health`);
|
|
195
|
+
const json = (await res.json()) as ServerHealth;
|
|
196
|
+
return {
|
|
197
|
+
ok: res.ok,
|
|
198
|
+
httpStatus: res.status,
|
|
199
|
+
data: json,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function toToolResult(data: unknown): ToolResult {
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default function register(api: PluginApi) {
|
|
210
|
+
const cfg = api.pluginConfig ?? (api.config as unknown as PluginConfig);
|
|
211
|
+
const port = cfg.port || 9377;
|
|
212
|
+
const baseUrl = cfg.url || `http://localhost:${port}`;
|
|
213
|
+
const autoStart = cfg.autoStart !== false; // default true
|
|
214
|
+
const pluginDir = getPluginDir();
|
|
215
|
+
const fallbackUserId = `camofox-${randomUUID()}`;
|
|
216
|
+
|
|
217
|
+
// Auto-start server if configured (default: true)
|
|
218
|
+
if (autoStart) {
|
|
219
|
+
(async () => {
|
|
220
|
+
const alreadyRunning = await checkServerRunning(baseUrl);
|
|
221
|
+
if (alreadyRunning) {
|
|
222
|
+
api.log?.info?.(`Camoufox server already running at ${baseUrl}`);
|
|
223
|
+
} else {
|
|
224
|
+
try {
|
|
225
|
+
serverProcess = await startServer(pluginDir, port, api.log);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
api.log?.error?.(`Failed to auto-start server: ${(err as Error).message}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
234
|
+
name: "camofox_create_tab",
|
|
235
|
+
description:
|
|
236
|
+
"PREFERRED: Create a new browser tab using Camoufox anti-detection browser. Use camofox tools instead of Chrome/built-in browser - they bypass bot detection on Google, Amazon, LinkedIn, etc. Returns tabId for subsequent operations.",
|
|
237
|
+
parameters: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
url: { type: "string", description: "Initial URL to navigate to" },
|
|
241
|
+
},
|
|
242
|
+
required: ["url"],
|
|
243
|
+
},
|
|
244
|
+
async execute(_id, params) {
|
|
245
|
+
const sessionKey = ctx.sessionKey || "default";
|
|
246
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
247
|
+
const result = await fetchApi(baseUrl, "/tabs", {
|
|
248
|
+
method: "POST",
|
|
249
|
+
body: JSON.stringify({ ...params, userId, sessionKey }),
|
|
250
|
+
});
|
|
251
|
+
return toToolResult(result);
|
|
252
|
+
},
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
256
|
+
name: "camofox_snapshot",
|
|
257
|
+
description:
|
|
258
|
+
"Get accessibility snapshot of a Camoufox page with element refs (e1, e2, etc.) for interaction. Large snapshots are truncated/windowed; use offset + nextOffset to paginate.",
|
|
259
|
+
parameters: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: {
|
|
262
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
263
|
+
offset: { type: "number", description: "Character offset for paginated snapshots. Use nextOffset from a previous truncated response." },
|
|
264
|
+
},
|
|
265
|
+
required: ["tabId"],
|
|
266
|
+
},
|
|
267
|
+
async execute(_id, params) {
|
|
268
|
+
const { tabId, offset } = params as { tabId: string; offset?: number };
|
|
269
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
270
|
+
const queryOffset = Number.isFinite(offset) ? Number(offset) : 0;
|
|
271
|
+
const result = (await fetchApi(baseUrl, `/tabs/${tabId}/snapshot?userId=${userId}&offset=${queryOffset}`)) as {
|
|
272
|
+
url?: string;
|
|
273
|
+
refsCount?: number;
|
|
274
|
+
snapshot?: string;
|
|
275
|
+
truncated?: boolean;
|
|
276
|
+
totalChars?: number;
|
|
277
|
+
hasMore?: boolean;
|
|
278
|
+
nextOffset?: number | null;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const text = [
|
|
282
|
+
`url: ${result.url || ""}`,
|
|
283
|
+
`refsCount: ${result.refsCount ?? 0}`,
|
|
284
|
+
`truncated: ${result.truncated ? "true" : "false"}`,
|
|
285
|
+
`totalChars: ${result.totalChars ?? 0}`,
|
|
286
|
+
`hasMore: ${result.hasMore ? "true" : "false"}`,
|
|
287
|
+
`nextOffset: ${result.nextOffset ?? "null"}`,
|
|
288
|
+
"",
|
|
289
|
+
result.snapshot || "",
|
|
290
|
+
].join("\n");
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: "text", text }],
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
}));
|
|
297
|
+
|
|
298
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
299
|
+
name: "camofox_console",
|
|
300
|
+
description:
|
|
301
|
+
"View browser console messages from a Camoufox tab. Returns console.log, console.warn, console.error, etc. messages captured since tab creation.",
|
|
302
|
+
parameters: {
|
|
303
|
+
type: "object",
|
|
304
|
+
properties: {
|
|
305
|
+
tabId: { type: "string", description: "Tab ID to get console messages from" },
|
|
306
|
+
type: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "Filter by type: log, warning, error, info, debug",
|
|
309
|
+
enum: ["log", "warning", "error", "info", "debug"],
|
|
310
|
+
},
|
|
311
|
+
limit: { type: "number", description: "Maximum number of messages to return (default 100)" },
|
|
312
|
+
userId: { type: "string", description: "User/session ID (optional, defaults to current agent)" },
|
|
313
|
+
},
|
|
314
|
+
required: ["tabId"],
|
|
315
|
+
},
|
|
316
|
+
async execute(_id, params) {
|
|
317
|
+
const args = params as { tabId: string; type?: string; limit?: number; userId?: string };
|
|
318
|
+
const userId = args.userId || ctx.agentId || fallbackUserId;
|
|
319
|
+
const query = new URLSearchParams({ userId });
|
|
320
|
+
if (args.type) query.set("type", args.type);
|
|
321
|
+
if (typeof args.limit === "number") query.set("limit", String(args.limit));
|
|
322
|
+
const result = await fetchApi(baseUrl, `/tabs/${args.tabId}/console?${query.toString()}`);
|
|
323
|
+
return toToolResult(result);
|
|
324
|
+
},
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
328
|
+
name: "camofox_errors",
|
|
329
|
+
description:
|
|
330
|
+
"View uncaught JavaScript errors from a Camoufox tab. Returns pageerror events (unhandled exceptions, failed promises) captured since tab creation.",
|
|
331
|
+
parameters: {
|
|
332
|
+
type: "object",
|
|
333
|
+
properties: {
|
|
334
|
+
tabId: { type: "string", description: "Tab ID to get errors from" },
|
|
335
|
+
limit: { type: "number", description: "Maximum number of errors to return (default 100)" },
|
|
336
|
+
userId: { type: "string", description: "User/session ID (optional, defaults to current agent)" },
|
|
337
|
+
},
|
|
338
|
+
required: ["tabId"],
|
|
339
|
+
},
|
|
340
|
+
async execute(_id, params) {
|
|
341
|
+
const args = params as { tabId: string; limit?: number; userId?: string };
|
|
342
|
+
const userId = args.userId || ctx.agentId || fallbackUserId;
|
|
343
|
+
const query = new URLSearchParams({ userId });
|
|
344
|
+
if (typeof args.limit === "number") query.set("limit", String(args.limit));
|
|
345
|
+
const result = await fetchApi(baseUrl, `/tabs/${args.tabId}/errors?${query.toString()}`);
|
|
346
|
+
return toToolResult(result);
|
|
347
|
+
},
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
351
|
+
name: "camofox_console_clear",
|
|
352
|
+
description:
|
|
353
|
+
"Clear captured console messages and page errors for a Camoufox tab.",
|
|
354
|
+
parameters: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
tabId: { type: "string", description: "Tab ID to clear console and errors for" },
|
|
358
|
+
userId: { type: "string", description: "User/session ID (optional, defaults to current agent)" },
|
|
359
|
+
},
|
|
360
|
+
required: ["tabId"],
|
|
361
|
+
},
|
|
362
|
+
async execute(_id, params) {
|
|
363
|
+
const args = params as { tabId: string; userId?: string };
|
|
364
|
+
const result = await fetchApi(baseUrl, `/tabs/${args.tabId}/console/clear`, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
body: JSON.stringify({
|
|
367
|
+
userId: args.userId || ctx.agentId || fallbackUserId,
|
|
368
|
+
}),
|
|
369
|
+
});
|
|
370
|
+
return toToolResult(result);
|
|
371
|
+
},
|
|
372
|
+
}));
|
|
373
|
+
|
|
374
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
375
|
+
name: "camofox_trace_start",
|
|
376
|
+
description:
|
|
377
|
+
"Start Playwright trace recording on a Camoufox session. Captures screenshots, DOM snapshots, and network activity. Output is a ZIP file viewable at trace.playwright.dev",
|
|
378
|
+
parameters: {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: {
|
|
381
|
+
tabId: { type: "string", description: "Tab ID to trace" },
|
|
382
|
+
screenshots: { type: "boolean", description: "Include screenshots (default true)" },
|
|
383
|
+
snapshots: { type: "boolean", description: "Include DOM snapshots (default true)" },
|
|
384
|
+
},
|
|
385
|
+
required: ["tabId"],
|
|
386
|
+
},
|
|
387
|
+
async execute(_id, params) {
|
|
388
|
+
const args = params as { tabId: string; userId?: string; screenshots?: boolean; snapshots?: boolean };
|
|
389
|
+
const result = await fetchApi(baseUrl, `/tabs/${args.tabId}/trace/start`, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
body: JSON.stringify({
|
|
392
|
+
userId: args.userId || ctx.agentId || fallbackUserId,
|
|
393
|
+
screenshots: args.screenshots ?? true,
|
|
394
|
+
snapshots: args.snapshots ?? true,
|
|
395
|
+
}),
|
|
396
|
+
});
|
|
397
|
+
return toToolResult(result);
|
|
398
|
+
},
|
|
399
|
+
}));
|
|
400
|
+
|
|
401
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
402
|
+
name: "camofox_trace_stop",
|
|
403
|
+
description:
|
|
404
|
+
"Stop Playwright trace recording and save the trace ZIP file. Opens at trace.playwright.dev for visual debugging.",
|
|
405
|
+
parameters: {
|
|
406
|
+
type: "object",
|
|
407
|
+
properties: {
|
|
408
|
+
tabId: { type: "string", description: "Tab ID" },
|
|
409
|
+
outputPath: { type: "string", description: "Output path for trace ZIP file" },
|
|
410
|
+
},
|
|
411
|
+
required: ["tabId"],
|
|
412
|
+
},
|
|
413
|
+
async execute(_id, params) {
|
|
414
|
+
const args = params as { tabId: string; userId?: string; outputPath?: string };
|
|
415
|
+
const result = await fetchApi(baseUrl, `/tabs/${args.tabId}/trace/stop`, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
body: JSON.stringify({
|
|
418
|
+
userId: args.userId || ctx.agentId || fallbackUserId,
|
|
419
|
+
path: args.outputPath,
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
return toToolResult(result);
|
|
423
|
+
},
|
|
424
|
+
}));
|
|
425
|
+
|
|
426
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
427
|
+
name: "camofox_click",
|
|
428
|
+
description: "Click an element in a Camoufox tab by ref (e.g., e1) or CSS selector.",
|
|
429
|
+
parameters: {
|
|
430
|
+
type: "object",
|
|
431
|
+
properties: {
|
|
432
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
433
|
+
ref: { type: "string", description: "Element ref from snapshot (e.g., e1)" },
|
|
434
|
+
selector: { type: "string", description: "CSS selector (alternative to ref)" },
|
|
435
|
+
},
|
|
436
|
+
required: ["tabId"],
|
|
437
|
+
},
|
|
438
|
+
async execute(_id, params) {
|
|
439
|
+
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
440
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
441
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/click`, {
|
|
442
|
+
method: "POST",
|
|
443
|
+
body: JSON.stringify({ ...rest, userId }),
|
|
444
|
+
});
|
|
445
|
+
return toToolResult(result);
|
|
446
|
+
},
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
450
|
+
name: "camofox_type",
|
|
451
|
+
description: "Type text into an element in a Camoufox tab.",
|
|
452
|
+
parameters: {
|
|
453
|
+
type: "object",
|
|
454
|
+
properties: {
|
|
455
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
456
|
+
ref: { type: "string", description: "Element ref from snapshot (e.g., e2)" },
|
|
457
|
+
selector: { type: "string", description: "CSS selector (alternative to ref)" },
|
|
458
|
+
text: { type: "string", description: "Text to type" },
|
|
459
|
+
pressEnter: { type: "boolean", description: "Press Enter after typing (submit)" },
|
|
460
|
+
},
|
|
461
|
+
required: ["tabId", "text"],
|
|
462
|
+
},
|
|
463
|
+
async execute(_id, params) {
|
|
464
|
+
const { tabId, pressEnter, ...rest } = params as { tabId: string; pressEnter?: boolean } & Record<string, unknown>;
|
|
465
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
466
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/type`, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
body: JSON.stringify({ ...rest, userId }),
|
|
469
|
+
});
|
|
470
|
+
if (pressEnter) {
|
|
471
|
+
await fetchApi(baseUrl, `/tabs/${tabId}/press`, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
body: JSON.stringify({ userId, key: "Enter" }),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return toToolResult(result);
|
|
477
|
+
},
|
|
478
|
+
}));
|
|
479
|
+
|
|
480
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
481
|
+
name: "camofox_navigate",
|
|
482
|
+
description:
|
|
483
|
+
"Navigate a Camoufox tab to a URL or use a search macro (@google_search, @youtube_search, etc.). Preferred over Chrome for sites with bot detection.",
|
|
484
|
+
parameters: {
|
|
485
|
+
type: "object",
|
|
486
|
+
properties: {
|
|
487
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
488
|
+
url: { type: "string", description: "URL to navigate to" },
|
|
489
|
+
macro: {
|
|
490
|
+
type: "string",
|
|
491
|
+
description: "Search macro (e.g., @google_search, @youtube_search)",
|
|
492
|
+
enum: [
|
|
493
|
+
"@google_search",
|
|
494
|
+
"@youtube_search",
|
|
495
|
+
"@amazon_search",
|
|
496
|
+
"@reddit_search",
|
|
497
|
+
"@reddit_subreddit",
|
|
498
|
+
"@wikipedia_search",
|
|
499
|
+
"@twitter_search",
|
|
500
|
+
"@yelp_search",
|
|
501
|
+
"@spotify_search",
|
|
502
|
+
"@netflix_search",
|
|
503
|
+
"@linkedin_search",
|
|
504
|
+
"@instagram_search",
|
|
505
|
+
"@tiktok_search",
|
|
506
|
+
"@twitch_search",
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
query: { type: "string", description: "Search query (when using macro)" },
|
|
510
|
+
},
|
|
511
|
+
required: ["tabId"],
|
|
512
|
+
},
|
|
513
|
+
async execute(_id, params) {
|
|
514
|
+
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
515
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
516
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/navigate`, {
|
|
517
|
+
method: "POST",
|
|
518
|
+
body: JSON.stringify({ ...rest, userId }),
|
|
519
|
+
});
|
|
520
|
+
return toToolResult(result);
|
|
521
|
+
},
|
|
522
|
+
}));
|
|
523
|
+
|
|
524
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
525
|
+
name: "camofox_go_back",
|
|
526
|
+
description: "Navigate back in browser history.",
|
|
527
|
+
parameters: {
|
|
528
|
+
type: "object",
|
|
529
|
+
properties: {
|
|
530
|
+
tabId: { type: "string", description: "Tab ID" },
|
|
531
|
+
},
|
|
532
|
+
required: ["tabId"],
|
|
533
|
+
},
|
|
534
|
+
async execute(_id, params) {
|
|
535
|
+
const { tabId } = params as { tabId: string };
|
|
536
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
537
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/back`, {
|
|
538
|
+
method: "POST",
|
|
539
|
+
body: JSON.stringify({ userId }),
|
|
540
|
+
});
|
|
541
|
+
return toToolResult(result);
|
|
542
|
+
},
|
|
543
|
+
}));
|
|
544
|
+
|
|
545
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
546
|
+
name: "camofox_go_forward",
|
|
547
|
+
description: "Navigate forward in browser history.",
|
|
548
|
+
parameters: {
|
|
549
|
+
type: "object",
|
|
550
|
+
properties: {
|
|
551
|
+
tabId: { type: "string", description: "Tab ID" },
|
|
552
|
+
},
|
|
553
|
+
required: ["tabId"],
|
|
554
|
+
},
|
|
555
|
+
async execute(_id, params) {
|
|
556
|
+
const { tabId } = params as { tabId: string };
|
|
557
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
558
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/forward`, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
body: JSON.stringify({ userId }),
|
|
561
|
+
});
|
|
562
|
+
return toToolResult(result);
|
|
563
|
+
},
|
|
564
|
+
}));
|
|
565
|
+
|
|
566
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
567
|
+
name: "camofox_refresh",
|
|
568
|
+
description: "Refresh the current page.",
|
|
569
|
+
parameters: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: {
|
|
572
|
+
tabId: { type: "string", description: "Tab ID" },
|
|
573
|
+
},
|
|
574
|
+
required: ["tabId"],
|
|
575
|
+
},
|
|
576
|
+
async execute(_id, params) {
|
|
577
|
+
const { tabId } = params as { tabId: string };
|
|
578
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
579
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/refresh`, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
body: JSON.stringify({ userId }),
|
|
582
|
+
});
|
|
583
|
+
return toToolResult(result);
|
|
584
|
+
},
|
|
585
|
+
}));
|
|
586
|
+
|
|
587
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
588
|
+
name: "camofox_scroll",
|
|
589
|
+
description: "Scroll a Camoufox page.",
|
|
590
|
+
parameters: {
|
|
591
|
+
type: "object",
|
|
592
|
+
properties: {
|
|
593
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
594
|
+
direction: { type: "string", enum: ["up", "down", "left", "right"] },
|
|
595
|
+
amount: { type: "number", description: "Pixels to scroll" },
|
|
596
|
+
},
|
|
597
|
+
required: ["tabId", "direction"],
|
|
598
|
+
},
|
|
599
|
+
async execute(_id, params) {
|
|
600
|
+
const { tabId, ...rest } = params as { tabId: string } & Record<string, unknown>;
|
|
601
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
602
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}/scroll`, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
body: JSON.stringify({ ...rest, userId }),
|
|
605
|
+
});
|
|
606
|
+
return toToolResult(result);
|
|
607
|
+
},
|
|
608
|
+
}));
|
|
609
|
+
|
|
610
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
611
|
+
name: "camofox_screenshot",
|
|
612
|
+
description: "Take a screenshot of a Camoufox page.",
|
|
613
|
+
parameters: {
|
|
614
|
+
type: "object",
|
|
615
|
+
properties: {
|
|
616
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
617
|
+
},
|
|
618
|
+
required: ["tabId"],
|
|
619
|
+
},
|
|
620
|
+
async execute(_id, params) {
|
|
621
|
+
const { tabId } = params as { tabId: string };
|
|
622
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
623
|
+
const url = `${baseUrl}/tabs/${tabId}/screenshot?userId=${userId}`;
|
|
624
|
+
const res = await fetch(url);
|
|
625
|
+
if (!res.ok) {
|
|
626
|
+
const text = await res.text();
|
|
627
|
+
throw new Error(`${res.status}: ${text}`);
|
|
628
|
+
}
|
|
629
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
630
|
+
const base64 = Buffer.from(arrayBuffer).toString("base64");
|
|
631
|
+
return {
|
|
632
|
+
content: [
|
|
633
|
+
{
|
|
634
|
+
type: "image",
|
|
635
|
+
data: base64,
|
|
636
|
+
mimeType: "image/png",
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
};
|
|
640
|
+
},
|
|
641
|
+
}));
|
|
642
|
+
|
|
643
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
644
|
+
name: "camofox_close_tab",
|
|
645
|
+
description: "Close a Camoufox browser tab.",
|
|
646
|
+
parameters: {
|
|
647
|
+
type: "object",
|
|
648
|
+
properties: {
|
|
649
|
+
tabId: { type: "string", description: "Tab identifier" },
|
|
650
|
+
},
|
|
651
|
+
required: ["tabId"],
|
|
652
|
+
},
|
|
653
|
+
async execute(_id, params) {
|
|
654
|
+
const { tabId } = params as { tabId: string };
|
|
655
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
656
|
+
const result = await fetchApi(baseUrl, `/tabs/${tabId}?userId=${userId}`, {
|
|
657
|
+
method: "DELETE",
|
|
658
|
+
});
|
|
659
|
+
return toToolResult(result);
|
|
660
|
+
},
|
|
661
|
+
}));
|
|
662
|
+
|
|
663
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
664
|
+
name: "camofox_list_tabs",
|
|
665
|
+
description: "List all open Camoufox tabs for a user.",
|
|
666
|
+
parameters: {
|
|
667
|
+
type: "object",
|
|
668
|
+
properties: {},
|
|
669
|
+
required: [],
|
|
670
|
+
},
|
|
671
|
+
async execute(_id, _params) {
|
|
672
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
673
|
+
const result = await fetchApi(baseUrl, `/tabs?userId=${userId}`);
|
|
674
|
+
return toToolResult(result);
|
|
675
|
+
},
|
|
676
|
+
}));
|
|
677
|
+
|
|
678
|
+
api.registerTool((ctx: ToolContext) => ({
|
|
679
|
+
name: "camofox_import_cookies",
|
|
680
|
+
description:
|
|
681
|
+
"Import cookies into the current Camoufox user session (Netscape cookie file). Use to authenticate to sites like LinkedIn without interactive login.",
|
|
682
|
+
parameters: {
|
|
683
|
+
type: "object",
|
|
684
|
+
properties: {
|
|
685
|
+
cookiesPath: { type: "string", description: "Path to Netscape-format cookies.txt file" },
|
|
686
|
+
domainSuffix: {
|
|
687
|
+
type: "string",
|
|
688
|
+
description: "Only import cookies whose domain ends with this suffix",
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
required: ["cookiesPath"],
|
|
692
|
+
},
|
|
693
|
+
async execute(_id, params) {
|
|
694
|
+
const { cookiesPath, domainSuffix } = params as {
|
|
695
|
+
cookiesPath: string;
|
|
696
|
+
domainSuffix?: string;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const userId = ctx.agentId || fallbackUserId;
|
|
700
|
+
|
|
701
|
+
const envCfg = loadConfig();
|
|
702
|
+
const cookiesDir = resolve(envCfg.cookiesDir);
|
|
703
|
+
|
|
704
|
+
const pwCookies = await readCookieFile({
|
|
705
|
+
cookiesDir,
|
|
706
|
+
cookiesPath,
|
|
707
|
+
domainSuffix,
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const result = await fetchApi(baseUrl, `/sessions/${encodeURIComponent(userId)}/cookies`, {
|
|
711
|
+
method: "POST",
|
|
712
|
+
body: JSON.stringify({ cookies: pwCookies }),
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
return toToolResult({ imported: pwCookies.length, userId, result });
|
|
716
|
+
},
|
|
717
|
+
}));
|
|
718
|
+
|
|
719
|
+
api.registerCommand({
|
|
720
|
+
name: "camofox",
|
|
721
|
+
description: "Camoufox browser server control (status, start, stop)",
|
|
722
|
+
handler: async (args) => {
|
|
723
|
+
const subcommand = args[0] || "status";
|
|
724
|
+
switch (subcommand) {
|
|
725
|
+
case "status":
|
|
726
|
+
try {
|
|
727
|
+
const health = await fetchApi(baseUrl, "/health");
|
|
728
|
+
api.log?.info?.(`Camoufox server at ${baseUrl}: ${JSON.stringify(health)}`);
|
|
729
|
+
} catch {
|
|
730
|
+
api.log?.error?.(`Camoufox server at ${baseUrl}: not reachable`);
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
733
|
+
case "start":
|
|
734
|
+
if (serverProcess) {
|
|
735
|
+
api.log?.info?.("Camoufox server already running (managed)");
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (await checkServerRunning(baseUrl)) {
|
|
739
|
+
api.log?.info?.(`Camoufox server already running at ${baseUrl}`);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
serverProcess = await startServer(pluginDir, port, api.log);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
api.log?.error?.(`Failed to start server: ${(err as Error).message}`);
|
|
746
|
+
}
|
|
747
|
+
break;
|
|
748
|
+
case "stop":
|
|
749
|
+
if (serverProcess) {
|
|
750
|
+
serverProcess.kill();
|
|
751
|
+
serverProcess = null;
|
|
752
|
+
api.log?.info?.("Stopped camofox-browser server");
|
|
753
|
+
} else {
|
|
754
|
+
api.log?.info?.("No managed server process running");
|
|
755
|
+
}
|
|
756
|
+
break;
|
|
757
|
+
default:
|
|
758
|
+
api.log?.error?.(`Unknown subcommand: ${subcommand}. Use: status, start, stop`);
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// Register health check for openclaw doctor/status
|
|
764
|
+
if (api.registerHealthCheck) {
|
|
765
|
+
api.registerHealthCheck("camofox-browser", async () => {
|
|
766
|
+
try {
|
|
767
|
+
const healthResp = await fetchHealth(baseUrl);
|
|
768
|
+
const health = healthResp.data;
|
|
769
|
+
return {
|
|
770
|
+
status: healthResp.ok ? "ok" : "warn",
|
|
771
|
+
message: `Server ${health.status || "unknown"} (HTTP ${healthResp.httpStatus})`,
|
|
772
|
+
details: {
|
|
773
|
+
url: baseUrl,
|
|
774
|
+
status: health.status,
|
|
775
|
+
consecutiveFailures: health.consecutiveFailures,
|
|
776
|
+
activeOps: health.activeOps,
|
|
777
|
+
tabs: health.tabs,
|
|
778
|
+
sessions: health.sessions,
|
|
779
|
+
managed: serverProcess !== null,
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
} catch {
|
|
783
|
+
return {
|
|
784
|
+
status: serverProcess ? "warn" : "error",
|
|
785
|
+
message: serverProcess
|
|
786
|
+
? "Server starting..."
|
|
787
|
+
: `Server not reachable at ${baseUrl}`,
|
|
788
|
+
details: {
|
|
789
|
+
url: baseUrl,
|
|
790
|
+
managed: serverProcess !== null,
|
|
791
|
+
hint: "Run: openclaw camofox start",
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Register RPC methods for gateway integration
|
|
799
|
+
if (api.registerRpc) {
|
|
800
|
+
api.registerRpc("camofox.health", async () => {
|
|
801
|
+
try {
|
|
802
|
+
const healthResp = await fetchHealth(baseUrl);
|
|
803
|
+
return {
|
|
804
|
+
status: healthResp.ok ? "ok" : "degraded",
|
|
805
|
+
httpStatus: healthResp.httpStatus,
|
|
806
|
+
...healthResp.data,
|
|
807
|
+
};
|
|
808
|
+
} catch (err) {
|
|
809
|
+
return { status: "error", error: (err as Error).message };
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
api.registerRpc("camofox.status", async () => {
|
|
814
|
+
const running = await checkServerRunning(baseUrl);
|
|
815
|
+
return {
|
|
816
|
+
running,
|
|
817
|
+
managed: serverProcess !== null,
|
|
818
|
+
pid: serverProcess?.pid || null,
|
|
819
|
+
url: baseUrl,
|
|
820
|
+
port,
|
|
821
|
+
};
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Register CLI subcommands (openclaw camofox ...)
|
|
826
|
+
if (api.registerCli) {
|
|
827
|
+
api.registerCli(
|
|
828
|
+
({ program }) => {
|
|
829
|
+
const camofox = program
|
|
830
|
+
.command("camofox")
|
|
831
|
+
.description("Camoufox anti-detection browser automation");
|
|
832
|
+
|
|
833
|
+
camofox
|
|
834
|
+
.command("status")
|
|
835
|
+
.description("Show server status")
|
|
836
|
+
.action(async () => {
|
|
837
|
+
try {
|
|
838
|
+
const healthResp = await fetchHealth(baseUrl);
|
|
839
|
+
const health = healthResp.data;
|
|
840
|
+
console.log(`Camoufox server: ${health.status || "unknown"} (HTTP ${healthResp.httpStatus})`);
|
|
841
|
+
console.log(` URL: ${baseUrl}`);
|
|
842
|
+
console.log(` Consecutive failures: ${health.consecutiveFailures ?? 0}`);
|
|
843
|
+
console.log(` Active ops: ${health.activeOps ?? 0}`);
|
|
844
|
+
console.log(` Tabs: ${health.tabs ?? 0}`);
|
|
845
|
+
console.log(` Sessions: ${health.sessions ?? 0}`);
|
|
846
|
+
console.log(` Managed: ${serverProcess !== null}`);
|
|
847
|
+
} catch {
|
|
848
|
+
console.log(`Camoufox server: not reachable`);
|
|
849
|
+
console.log(` URL: ${baseUrl}`);
|
|
850
|
+
console.log(` Managed: ${serverProcess !== null}`);
|
|
851
|
+
console.log(` Hint: Run 'openclaw camofox start' to start the server`);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
camofox
|
|
856
|
+
.command("start")
|
|
857
|
+
.description("Start the camofox server")
|
|
858
|
+
.action(async () => {
|
|
859
|
+
if (serverProcess) {
|
|
860
|
+
console.log("Camoufox server already running (managed by plugin)");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (await checkServerRunning(baseUrl)) {
|
|
864
|
+
console.log(`Camoufox server already running at ${baseUrl}`);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
console.log(`Starting camofox server on port ${port}...`);
|
|
869
|
+
serverProcess = await startServer(pluginDir, port, api.log);
|
|
870
|
+
console.log(`Camoufox server started at ${baseUrl}`);
|
|
871
|
+
} catch (err) {
|
|
872
|
+
console.error(`Failed to start server: ${(err as Error).message}`);
|
|
873
|
+
process.exit(1);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
camofox
|
|
878
|
+
.command("stop")
|
|
879
|
+
.description("Stop the camofox server")
|
|
880
|
+
.action(async () => {
|
|
881
|
+
if (serverProcess) {
|
|
882
|
+
serverProcess.kill();
|
|
883
|
+
serverProcess = null;
|
|
884
|
+
console.log("Stopped camofox server");
|
|
885
|
+
} else {
|
|
886
|
+
console.log("No managed server process running");
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
camofox
|
|
891
|
+
.command("configure")
|
|
892
|
+
.description("Configure camofox plugin settings")
|
|
893
|
+
.action(async () => {
|
|
894
|
+
console.log("Camoufox Browser Configuration");
|
|
895
|
+
console.log("================================");
|
|
896
|
+
console.log("");
|
|
897
|
+
console.log("Current settings:");
|
|
898
|
+
console.log(` Server URL: ${baseUrl}`);
|
|
899
|
+
console.log(` Port: ${port}`);
|
|
900
|
+
console.log(` Auto-start: ${autoStart}`);
|
|
901
|
+
console.log("");
|
|
902
|
+
console.log("Plugin config (openclaw.json):");
|
|
903
|
+
console.log("");
|
|
904
|
+
console.log(" plugins:");
|
|
905
|
+
console.log(" entries:");
|
|
906
|
+
console.log(" camofox-browser:");
|
|
907
|
+
console.log(" enabled: true");
|
|
908
|
+
console.log(" config:");
|
|
909
|
+
console.log(" port: 9377");
|
|
910
|
+
console.log(" autoStart: true");
|
|
911
|
+
console.log("");
|
|
912
|
+
console.log("To use camofox as the ONLY browser tool, disable the built-in:");
|
|
913
|
+
console.log("");
|
|
914
|
+
console.log(" tools:");
|
|
915
|
+
console.log(' deny: ["browser"]');
|
|
916
|
+
console.log("");
|
|
917
|
+
console.log("This removes OpenClaw's built-in browser tool, leaving camofox tools.");
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
camofox
|
|
921
|
+
.command("tabs")
|
|
922
|
+
.description("List active browser tabs")
|
|
923
|
+
.option("--user <userId>", "Filter by user ID")
|
|
924
|
+
.action(async (opts: { user?: string }) => {
|
|
925
|
+
try {
|
|
926
|
+
const endpoint = opts.user ? `/tabs?userId=${opts.user}` : "/tabs";
|
|
927
|
+
const tabs = (await fetchApi(baseUrl, endpoint)) as Array<{
|
|
928
|
+
tabId: string;
|
|
929
|
+
userId: string;
|
|
930
|
+
url: string;
|
|
931
|
+
title: string;
|
|
932
|
+
}>;
|
|
933
|
+
if (tabs.length === 0) {
|
|
934
|
+
console.log("No active tabs");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
console.log(`Active tabs (${tabs.length}):`);
|
|
938
|
+
for (const tab of tabs) {
|
|
939
|
+
console.log(` ${tab.tabId} [${tab.userId}] ${tab.title || tab.url}`);
|
|
940
|
+
}
|
|
941
|
+
} catch (err) {
|
|
942
|
+
console.error(`Failed to list tabs: ${(err as Error).message}`);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
},
|
|
946
|
+
{ commands: ["camofox"] }
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|