talon-agent 1.6.1 → 1.7.0

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.
@@ -10,9 +10,16 @@
10
10
  * "browser": "chromium", // optional, default "chromium"
11
11
  * "headless": true // optional, default true
12
12
  * }
13
+ *
14
+ * For Camoufox (anti-detect browser):
15
+ * "playwright": {
16
+ * "enabled": true,
17
+ * "browser": "firefox",
18
+ * "endpointFile": "/home/dylan/camoufox-endpoint.txt"
19
+ * }
13
20
  */
14
21
 
15
- import { existsSync } from "node:fs";
22
+ import { existsSync, readFileSync } from "node:fs";
16
23
  import { resolve } from "node:path";
17
24
  import type { TalonPlugin } from "../../core/plugin.js";
18
25
  import { log } from "../../util/log.js";
@@ -20,29 +27,51 @@ import { log } from "../../util/log.js";
20
27
  export function createPlaywrightPlugin(config: {
21
28
  browser?: string;
22
29
  headless?: boolean;
30
+ endpoint?: string;
31
+ endpointFile?: string;
23
32
  }): TalonPlugin {
24
33
  const browser = config.browser ?? "chromium";
25
34
  const headless = config.headless !== false; // default true
26
35
 
36
+ // Resolve endpoint: direct string or read from file
37
+ let endpoint = config.endpoint;
38
+ if (!endpoint && config.endpointFile) {
39
+ try {
40
+ endpoint = readFileSync(config.endpointFile, "utf-8").trim();
41
+ } catch {
42
+ log(
43
+ "playwright",
44
+ `Warning: could not read endpoint file ${config.endpointFile}`,
45
+ );
46
+ }
47
+ }
48
+
27
49
  // Resolve path from Talon's node_modules
28
50
  const mcpBin = resolve(
29
51
  import.meta.dirname ?? ".",
30
52
  "../../../node_modules/@playwright/mcp/cli.js",
31
53
  );
32
54
 
33
- const args = ["--no-sandbox"];
55
+ const args: string[] = [];
34
56
 
35
- if (headless) {
36
- args.push("--headless");
37
- }
57
+ if (endpoint) {
58
+ // Connect to existing browser (e.g. Camoufox websocket server)
59
+ args.push("--endpoint", endpoint);
60
+ } else {
61
+ args.push("--no-sandbox");
62
+
63
+ if (headless) {
64
+ args.push("--headless");
65
+ }
38
66
 
39
- if (browser !== "chromium") {
40
- args.push("--browser", browser);
67
+ if (browser !== "chromium") {
68
+ args.push("--browser", browser);
69
+ }
41
70
  }
42
71
 
43
72
  return {
44
73
  name: "playwright",
45
- description: "Browser automation via Playwright MCP (headless Chromium)",
74
+ description: `Browser automation via Playwright MCP (${endpoint ? "Camoufox" : browser})`,
46
75
  version: "1.0.0",
47
76
 
48
77
  mcpServer: {
@@ -53,17 +82,19 @@ export function createPlaywrightPlugin(config: {
53
82
  validateConfig() {
54
83
  const errors: string[] = [];
55
84
 
56
- const validBrowsers = [
57
- "chromium",
58
- "chrome",
59
- "firefox",
60
- "webkit",
61
- "msedge",
62
- ];
63
- if (!validBrowsers.includes(browser)) {
64
- errors.push(
65
- `Invalid browser "${browser}". Valid options: ${validBrowsers.join(", ")}`,
66
- );
85
+ if (!endpoint) {
86
+ const validBrowsers = [
87
+ "chromium",
88
+ "chrome",
89
+ "firefox",
90
+ "webkit",
91
+ "msedge",
92
+ ];
93
+ if (!validBrowsers.includes(browser)) {
94
+ errors.push(
95
+ `Invalid browser "${browser}". Valid options: ${validBrowsers.join(", ")}`,
96
+ );
97
+ }
67
98
  }
68
99
 
69
100
  if (!existsSync(mcpBin)) {
@@ -76,7 +107,10 @@ export function createPlaywrightPlugin(config: {
76
107
  },
77
108
 
78
109
  async init() {
79
- log("playwright", `Ready (${browser}, headless=${headless})`);
110
+ log(
111
+ "playwright",
112
+ `Ready (${endpoint ? `Camoufox @ ${endpoint}` : `${browser}, headless=${headless}`})`,
113
+ );
80
114
  },
81
115
  };
82
116
  }
@@ -14,19 +14,110 @@ import { log } from "./log.js";
14
14
 
15
15
  // ── Config schema ───────────────────────────────────────────────────────────
16
16
 
17
- const pluginEntrySchema = z.object({
18
- path: z.string(),
19
- config: z.record(z.string(), z.unknown()).optional(),
20
- });
17
+ /** Path-based Talon plugin (loaded as a Node module). */
18
+ const pluginPathSchema = z
19
+ .object({
20
+ path: z.string(),
21
+ config: z.record(z.string(), z.unknown()).optional(),
22
+ })
23
+ .strict();
24
+
25
+ /** Standalone MCP server (command + args, not a Talon plugin module). */
26
+ const pluginMcpSchema = z
27
+ .object({
28
+ name: z.string(),
29
+ command: z.string(),
30
+ args: z.array(z.string()).optional(),
31
+ env: z.record(z.string(), z.string()).optional(),
32
+ })
33
+ .strict();
34
+
35
+ const pluginEntrySchema = z
36
+ .object({
37
+ path: z.string().optional(),
38
+ config: z.record(z.string(), z.unknown()).optional(),
39
+ name: z.string().optional(),
40
+ command: z.string().optional(),
41
+ args: z.array(z.string()).optional(),
42
+ env: z.record(z.string(), z.string()).optional(),
43
+ })
44
+ .strict()
45
+ .superRefine((value, ctx) => {
46
+ const hasPath = value.path !== undefined;
47
+ const hasMcpFields =
48
+ value.name !== undefined ||
49
+ value.command !== undefined ||
50
+ value.args !== undefined ||
51
+ value.env !== undefined;
52
+
53
+ if (hasPath && hasMcpFields) {
54
+ ctx.addIssue({
55
+ code: z.ZodIssueCode.custom,
56
+ message:
57
+ "Plugin entry must use exactly one format: either 'path' (with optional 'config') or MCP fields ('name', 'command', optional 'args'/'env'), but not both.",
58
+ });
59
+ return;
60
+ }
61
+
62
+ if (!hasPath && !hasMcpFields) {
63
+ ctx.addIssue({
64
+ code: z.ZodIssueCode.custom,
65
+ message:
66
+ "Plugin entry must provide either 'path' or both 'name' and 'command'.",
67
+ });
68
+ return;
69
+ }
70
+
71
+ if (hasMcpFields) {
72
+ if (value.config !== undefined) {
73
+ ctx.addIssue({
74
+ code: z.ZodIssueCode.custom,
75
+ path: ["config"],
76
+ message: "MCP plugin entries cannot include 'config'.",
77
+ });
78
+ }
79
+
80
+ if (value.name === undefined) {
81
+ ctx.addIssue({
82
+ code: z.ZodIssueCode.custom,
83
+ path: ["name"],
84
+ message: "MCP plugin entries must include 'name'.",
85
+ });
86
+ }
87
+
88
+ if (value.command === undefined) {
89
+ ctx.addIssue({
90
+ code: z.ZodIssueCode.custom,
91
+ path: ["command"],
92
+ message: "MCP plugin entries must include 'command'.",
93
+ });
94
+ }
95
+
96
+ return;
97
+ }
98
+ })
99
+ .pipe(z.union([pluginPathSchema, pluginMcpSchema]));
21
100
 
22
101
  const frontendEnum = z.enum(["telegram", "terminal", "teams"]);
23
102
 
103
+ const playwrightConfigSchema = z.object({
104
+ enabled: z.boolean().default(false),
105
+ /** Browser engine: chromium (default), chrome, firefox, webkit, msedge */
106
+ browser: z.string().optional(),
107
+ /** Run headless (default: true) */
108
+ headless: z.boolean().default(true),
109
+ /** Connect to an existing browser websocket endpoint. */
110
+ endpoint: z.string().optional(),
111
+ /** Read the browser websocket endpoint from a file. */
112
+ endpointFile: z.string().optional(),
113
+ });
114
+
24
115
  const configSchema = z.object({
25
116
  frontend: z.union([frontendEnum, z.array(frontendEnum)]).default("telegram"),
26
117
  botToken: z.string().optional(),
27
118
  backend: z.enum(["claude", "opencode"]).default("claude"),
28
119
  claudeBinary: z.string().optional(),
29
- model: z.string().default("claude-sonnet-4-6"),
120
+ model: z.string().default("default"),
30
121
  dreamModel: z.string().optional(), // Model used for background memory consolidation (defaults to main model)
31
122
  maxMessageLength: z.number().int().min(100).default(4000),
32
123
  concurrency: z.number().int().min(1).max(20).default(1),
@@ -64,15 +155,7 @@ const configSchema = z.object({
64
155
  .optional(),
65
156
 
66
157
  // Playwright — headless browser automation via MCP
67
- playwright: z
68
- .object({
69
- enabled: z.boolean().default(false),
70
- /** Browser engine: chromium (default), chrome, firefox, webkit, msedge */
71
- browser: z.string().optional(),
72
- /** Run headless (default: true) */
73
- headless: z.boolean().default(true),
74
- })
75
- .optional(),
158
+ playwright: playwrightConfigSchema.optional(),
76
159
 
77
160
  // Display name shown in terminal UI (defaults to "Talon")
78
161
  botDisplayName: z.string().default("Talon"),
@@ -104,7 +187,7 @@ const CONFIG_FILE = pathFiles.config;
104
187
 
105
188
  const DEFAULT_CONFIG = {
106
189
  botToken: "",
107
- model: "claude-sonnet-4-6",
190
+ model: "default",
108
191
  maxMessageLength: 4000,
109
192
  concurrency: 1,
110
193
  pulse: true,