talon-agent 3.28.0 → 3.29.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.
- package/README.md +12 -9
- package/package.json +3 -3
- package/src/bootstrap.ts +22 -5
- package/src/core/doctor.ts +41 -0
- package/src/core/plugin/builtins.ts +81 -5
- package/src/core/plugin/native-runtimes.ts +87 -0
- package/src/core/plugin/provision-journal.ts +172 -0
- package/src/core/plugin/provision.ts +276 -0
- package/src/frontend/discord/commands/admin.ts +5 -0
- package/src/frontend/telegram/commands/admin.ts +5 -0
- package/src/plugins/github/index.ts +15 -17
- package/src/plugins/github/provision.ts +172 -0
- package/src/plugins/mempalace/index.ts +17 -3
- package/src/plugins/mempalace/provision.ts +667 -0
- package/src/plugins/playwright/provision.ts +322 -0
- package/src/util/config.ts +27 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright provisioner — browser builds present before first use.
|
|
3
|
+
*
|
|
4
|
+
* @playwright/mcp launches its browser lazily, so a missing build used
|
|
5
|
+
* to surface as a mid-conversation tool error. This makes it a boot
|
|
6
|
+
* concern instead: when the configured engine's build is absent from
|
|
7
|
+
* the Playwright cache, run the bundled playwright-core CLI's `install`
|
|
8
|
+
* (idempotent, version-matched to the pinned @playwright/mcp — the cli
|
|
9
|
+
* resolves from the same dependency tree the MCP server runs).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately out of scope, in line with never mutating what Talon
|
|
12
|
+
* doesn't own: system channels (chrome, msedge) — those belong to the
|
|
13
|
+
* OS; endpoint mode — the browser lives on the remote end.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
import { dirname, join, resolve } from "node:path";
|
|
19
|
+
import type { DoctorCheck } from "../../core/doctor.js";
|
|
20
|
+
import {
|
|
21
|
+
failDetail,
|
|
22
|
+
loadProvisionState,
|
|
23
|
+
markProvisionFailure,
|
|
24
|
+
markProvisionSuccess,
|
|
25
|
+
runStep,
|
|
26
|
+
shouldAttempt,
|
|
27
|
+
type ExecFn,
|
|
28
|
+
type ProvisionOutcome,
|
|
29
|
+
} from "../../core/plugin/provision.js";
|
|
30
|
+
import { dirs } from "../../util/paths.js";
|
|
31
|
+
|
|
32
|
+
/** Engines whose builds Playwright manages (vs system channels). */
|
|
33
|
+
const MANAGED_ENGINES = new Set(["chromium", "firefox", "webkit"]);
|
|
34
|
+
|
|
35
|
+
const INSTALL_TIMEOUT_MS = 600_000;
|
|
36
|
+
|
|
37
|
+
/** The `playwright` config section this module reads. */
|
|
38
|
+
export interface PlaywrightSection {
|
|
39
|
+
browser?: string;
|
|
40
|
+
endpoint?: string;
|
|
41
|
+
endpointFile?: string;
|
|
42
|
+
autoProvision?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PlaywrightProvisionDeps {
|
|
46
|
+
exec?: ExecFn;
|
|
47
|
+
platform?: NodeJS.Platform;
|
|
48
|
+
home?: string;
|
|
49
|
+
env?: Record<string, string | undefined>;
|
|
50
|
+
now?: () => number;
|
|
51
|
+
statePath?: string;
|
|
52
|
+
cliPath?: string;
|
|
53
|
+
pathExists?: (p: string) => boolean;
|
|
54
|
+
listDir?: (p: string) => string[];
|
|
55
|
+
/** playwright-core's browser registry (default: browsers.json beside the CLI). */
|
|
56
|
+
registry?: BrowserDescriptor[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One entry of playwright-core's browsers.json. */
|
|
60
|
+
export interface BrowserDescriptor {
|
|
61
|
+
name: string;
|
|
62
|
+
revision: string;
|
|
63
|
+
/** Per-host-platform revisions (e.g. webkit on older macOS). */
|
|
64
|
+
revisionOverrides?: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Where Playwright keeps downloaded builds, per OS — the same defaults
|
|
69
|
+
* playwright-core's registry uses, and the same override env var.
|
|
70
|
+
*/
|
|
71
|
+
export function browsersRoot(
|
|
72
|
+
platform: NodeJS.Platform,
|
|
73
|
+
home: string,
|
|
74
|
+
env: Record<string, string | undefined>,
|
|
75
|
+
): string {
|
|
76
|
+
const override = env.PLAYWRIGHT_BROWSERS_PATH;
|
|
77
|
+
if (override && override !== "0") return override;
|
|
78
|
+
if (platform === "darwin") {
|
|
79
|
+
return join(home, "Library", "Caches", "ms-playwright");
|
|
80
|
+
}
|
|
81
|
+
if (platform === "win32") {
|
|
82
|
+
return join(
|
|
83
|
+
env.LOCALAPPDATA ?? join(home, "AppData", "Local"),
|
|
84
|
+
"ms-playwright",
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return join(env.XDG_CACHE_HOME ?? join(home, ".cache"), "ms-playwright");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The bundled playwright-core CLI (version-matched to @playwright/mcp). */
|
|
91
|
+
function defaultCliPath(): string {
|
|
92
|
+
return resolve(
|
|
93
|
+
import.meta.dirname ?? ".",
|
|
94
|
+
"../../../node_modules/playwright-core/cli.js",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const safeListDir = (p: string): string[] => {
|
|
99
|
+
try {
|
|
100
|
+
return readdirSync(p);
|
|
101
|
+
} catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** browsers.json shipped beside the CLI — the revisions this playwright-core needs. */
|
|
107
|
+
function readRegistry(cli: string): BrowserDescriptor[] {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(
|
|
110
|
+
readFileSync(join(dirname(cli), "browsers.json"), "utf-8"),
|
|
111
|
+
) as { browsers?: BrowserDescriptor[] };
|
|
112
|
+
return Array.isArray(parsed.browsers) ? parsed.browsers : [];
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The build directories the registry requires for an engine: the engine
|
|
120
|
+
* itself plus its headless-shell companion (chromium runs headless via
|
|
121
|
+
* chromium-headless-shell; `install chromium` fetches both). Named the
|
|
122
|
+
* way playwright-core's registry names them — `<name with _>-<revision>`,
|
|
123
|
+
* or `<name>_<platform>_special-<revision>` for a per-platform override
|
|
124
|
+
* — and complete only once the registry's INSTALLATION_COMPLETE marker
|
|
125
|
+
* is present, so a stale revision from an older playwright-core or a
|
|
126
|
+
* half-downloaded build never passes for ready.
|
|
127
|
+
*/
|
|
128
|
+
function requiredBuildDirs(
|
|
129
|
+
browser: string,
|
|
130
|
+
registry: BrowserDescriptor[],
|
|
131
|
+
): string[][] {
|
|
132
|
+
return registry
|
|
133
|
+
.filter((d) => d.name === browser || d.name === `${browser}-headless-shell`)
|
|
134
|
+
.map((d) => {
|
|
135
|
+
const prefix = d.name.replaceAll("-", "_");
|
|
136
|
+
return [
|
|
137
|
+
`${prefix}-${d.revision}`,
|
|
138
|
+
...Object.entries(d.revisionOverrides ?? {}).map(
|
|
139
|
+
([platform, revision]) => `${prefix}_${platform}_special-${revision}`,
|
|
140
|
+
),
|
|
141
|
+
];
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether the engine's required builds are present and complete. Pure
|
|
147
|
+
* filesystem inspection, no subprocess. Without a readable registry
|
|
148
|
+
* (unexpected — it ships with playwright-core) any build directory for
|
|
149
|
+
* the engine counts, which is the pre-registry behavior.
|
|
150
|
+
*/
|
|
151
|
+
function browserPresent(
|
|
152
|
+
browser: string,
|
|
153
|
+
cli: string,
|
|
154
|
+
deps: Pick<
|
|
155
|
+
PlaywrightProvisionDeps,
|
|
156
|
+
"platform" | "home" | "env" | "listDir" | "pathExists" | "registry"
|
|
157
|
+
> = {},
|
|
158
|
+
): boolean {
|
|
159
|
+
const root = browsersRoot(
|
|
160
|
+
deps.platform ?? process.platform,
|
|
161
|
+
deps.home ?? homedir(),
|
|
162
|
+
deps.env ?? process.env,
|
|
163
|
+
);
|
|
164
|
+
const present = new Set((deps.listDir ?? safeListDir)(root));
|
|
165
|
+
const pathExists = deps.pathExists ?? existsSync;
|
|
166
|
+
const required = requiredBuildDirs(
|
|
167
|
+
browser,
|
|
168
|
+
deps.registry ?? readRegistry(cli),
|
|
169
|
+
);
|
|
170
|
+
if (required.length === 0) {
|
|
171
|
+
return [...present].some((name) => name.startsWith(browser));
|
|
172
|
+
}
|
|
173
|
+
return required.every((candidates) =>
|
|
174
|
+
candidates.some(
|
|
175
|
+
(dir) =>
|
|
176
|
+
present.has(dir) &&
|
|
177
|
+
pathExists(join(root, dir, "INSTALLATION_COMPLETE")),
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Forward the injected environment (e.g. PLAYWRIGHT_BROWSERS_PATH) to the CLI. */
|
|
183
|
+
function execEnv(
|
|
184
|
+
env: Record<string, string | undefined> | undefined,
|
|
185
|
+
): Record<string, string> | undefined {
|
|
186
|
+
if (!env) return undefined;
|
|
187
|
+
return Object.fromEntries(
|
|
188
|
+
Object.entries(env).filter(
|
|
189
|
+
(e): e is [string, string] => e[1] !== undefined,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function provisionPlaywright(
|
|
195
|
+
section: PlaywrightSection,
|
|
196
|
+
deps: PlaywrightProvisionDeps = {},
|
|
197
|
+
): Promise<ProvisionOutcome> {
|
|
198
|
+
const exec = deps.exec ?? runStep;
|
|
199
|
+
const now = deps.now ?? Date.now;
|
|
200
|
+
const pathExists = deps.pathExists ?? existsSync;
|
|
201
|
+
const browser = section.browser ?? "chromium";
|
|
202
|
+
|
|
203
|
+
if (section.endpoint ?? section.endpointFile) {
|
|
204
|
+
return { status: "skipped", kind: "endpoint", actions: [], warnings: [] };
|
|
205
|
+
}
|
|
206
|
+
if (!MANAGED_ENGINES.has(browser)) {
|
|
207
|
+
return {
|
|
208
|
+
status: "skipped",
|
|
209
|
+
kind: "system-channel",
|
|
210
|
+
actions: [],
|
|
211
|
+
warnings: [],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (section.autoProvision === false) {
|
|
215
|
+
return { status: "skipped", kind: "managed", actions: [], warnings: [] };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const cli = deps.cliPath ?? defaultCliPath();
|
|
219
|
+
if (browserPresent(browser, cli, deps)) {
|
|
220
|
+
return { status: "ready", kind: "managed", actions: [], warnings: [] };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!pathExists(cli)) {
|
|
224
|
+
return {
|
|
225
|
+
status: "failed",
|
|
226
|
+
kind: "managed",
|
|
227
|
+
actions: [],
|
|
228
|
+
warnings: [
|
|
229
|
+
`playwright-core CLI not found at ${cli} — reinstall dependencies (npm ci)`,
|
|
230
|
+
],
|
|
231
|
+
error: "playwright-core missing",
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const statePath =
|
|
236
|
+
deps.statePath ?? join(dirs.data, "playwright-provision.json");
|
|
237
|
+
const state = loadProvisionState(statePath);
|
|
238
|
+
// The pin key is the browser name: switching engines re-arms retries.
|
|
239
|
+
if (!shouldAttempt(state, browser, now())) {
|
|
240
|
+
return {
|
|
241
|
+
status: "degraded",
|
|
242
|
+
kind: "managed",
|
|
243
|
+
actions: [],
|
|
244
|
+
warnings: [
|
|
245
|
+
`last ${browser} download failed (${state.lastError ?? "unknown"}); retrying with backoff`,
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const installed = await exec(process.execPath, [cli, "install", browser], {
|
|
251
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
252
|
+
env: execEnv(deps.env),
|
|
253
|
+
});
|
|
254
|
+
if (!installed.ok) {
|
|
255
|
+
const detail = failDetail(installed);
|
|
256
|
+
// Linux-only failure class: the build downloaded but shared system
|
|
257
|
+
// libraries are missing — that fix needs root, so it stays advisory.
|
|
258
|
+
const depsHint = /missing dependencies|install-deps|apt-get/i.test(
|
|
259
|
+
installed.stderr,
|
|
260
|
+
)
|
|
261
|
+
? ` — system libraries are missing; run: sudo npx playwright install-deps ${browser}`
|
|
262
|
+
: "";
|
|
263
|
+
markProvisionFailure(statePath, state, browser, detail, now());
|
|
264
|
+
return {
|
|
265
|
+
status: "degraded",
|
|
266
|
+
kind: "managed",
|
|
267
|
+
actions: [],
|
|
268
|
+
warnings: [
|
|
269
|
+
`${browser} download failed (${detail})${depsHint} — browser tools will error until resolved`,
|
|
270
|
+
],
|
|
271
|
+
error: detail,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
markProvisionSuccess(statePath, state, browser, now());
|
|
276
|
+
return {
|
|
277
|
+
status: "ready",
|
|
278
|
+
kind: "managed",
|
|
279
|
+
actions: [`downloaded ${browser} browser build`],
|
|
280
|
+
warnings: [],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Read-only doctor inspection: build present / endpoint mode. */
|
|
285
|
+
export function inspectPlaywright(
|
|
286
|
+
section: PlaywrightSection,
|
|
287
|
+
deps: PlaywrightProvisionDeps = {},
|
|
288
|
+
): DoctorCheck[] {
|
|
289
|
+
if (section.endpoint ?? section.endpointFile) {
|
|
290
|
+
return [
|
|
291
|
+
{
|
|
292
|
+
label: "Playwright: remote endpoint mode",
|
|
293
|
+
status: "info",
|
|
294
|
+
detail: "browser lives on the remote end",
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
const browser = section.browser ?? "chromium";
|
|
299
|
+
if (!MANAGED_ENGINES.has(browser)) {
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
label: `Playwright: system channel (${browser})`,
|
|
303
|
+
status: "info",
|
|
304
|
+
detail: "browser managed by the OS",
|
|
305
|
+
},
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
const cli = deps.cliPath ?? defaultCliPath();
|
|
309
|
+
return [
|
|
310
|
+
browserPresent(browser, cli, deps)
|
|
311
|
+
? { label: `Playwright ${browser} build present`, status: "ok" }
|
|
312
|
+
: {
|
|
313
|
+
label: `Playwright ${browser} build missing`,
|
|
314
|
+
status: "warn",
|
|
315
|
+
detail:
|
|
316
|
+
section.autoProvision === false
|
|
317
|
+
? `automatic download disabled (playwright.autoProvision: false) — run: npx playwright install ${browser}`
|
|
318
|
+
: "downloads automatically at next talon start",
|
|
319
|
+
issue: true,
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
}
|
package/src/util/config.ts
CHANGED
|
@@ -267,6 +267,13 @@ const playwrightConfigSchema = z.object({
|
|
|
267
267
|
enabled: z.boolean().default(false),
|
|
268
268
|
/** Browser engine: chromium (default), chrome, firefox, webkit, msedge */
|
|
269
269
|
browser: z.string().optional(),
|
|
270
|
+
/**
|
|
271
|
+
* Download the browser build automatically when missing (default
|
|
272
|
+
* true). Applies to Playwright-managed engines (chromium, firefox,
|
|
273
|
+
* webkit); system channels (chrome, msedge) and endpoint mode are
|
|
274
|
+
* never touched.
|
|
275
|
+
*/
|
|
276
|
+
autoProvision: z.boolean().optional(),
|
|
270
277
|
/** Run headless (default: true) */
|
|
271
278
|
headless: z.boolean().default(true),
|
|
272
279
|
/** Connect to an existing browser websocket endpoint. */
|
|
@@ -289,6 +296,19 @@ const mempalaceSettingsSchema = z.object({
|
|
|
289
296
|
entityLanguages: z.array(z.string().min(2)).nonempty().optional(),
|
|
290
297
|
/** Enable mempalace diagnostic diaries (sets MEMPAL_VERBOSE=1). */
|
|
291
298
|
verbose: z.boolean().optional(),
|
|
299
|
+
/**
|
|
300
|
+
* Exact mempalace version for the Talon-managed venv (default: the
|
|
301
|
+
* built-in pin). Ignored for operator-managed installs (custom
|
|
302
|
+
* pythonPath) — those only get an advisory when they drift.
|
|
303
|
+
*/
|
|
304
|
+
version: z
|
|
305
|
+
.string()
|
|
306
|
+
.regex(/^\d+\.\d+\.\d+$/, "exact version, e.g. 3.8.0")
|
|
307
|
+
.optional(),
|
|
308
|
+
/** Reconcile the managed venv to the pinned version (default true). */
|
|
309
|
+
autoUpdate: z.boolean().optional(),
|
|
310
|
+
/** Create/heal the managed venv automatically (default true). */
|
|
311
|
+
autoProvision: z.boolean().optional(),
|
|
292
312
|
});
|
|
293
313
|
|
|
294
314
|
/** mem0 backend settings, shared by `memory.mem0` and the top-level `mem0` section. */
|
|
@@ -496,6 +516,13 @@ const configSchema = z.object({
|
|
|
496
516
|
enabled: z.boolean().default(false),
|
|
497
517
|
/** GitHub personal access token (default: from `gh auth token`) */
|
|
498
518
|
token: z.string().min(1).optional(),
|
|
519
|
+
/**
|
|
520
|
+
* github-mcp-server image tag (default: the built-in pin).
|
|
521
|
+
* "latest" opts out of pinning.
|
|
522
|
+
*/
|
|
523
|
+
imageTag: z.string().min(1).optional(),
|
|
524
|
+
/** Pull the pinned Docker image automatically (default true). */
|
|
525
|
+
autoProvision: z.boolean().optional(),
|
|
499
526
|
})
|
|
500
527
|
.optional(),
|
|
501
528
|
|