opencode-codex-control 0.0.0-tegami-trusted-publish-setup → 0.1.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/LICENSE +21 -0
- package/LICENSE-EXECUTOR +21 -0
- package/NOTICE +24 -0
- package/README.md +132 -6
- package/index.ts +1 -0
- package/package.json +54 -2
- package/src/codex/appserver.ts +415 -0
- package/src/codex/install.ts +122 -0
- package/src/codex/permissions.ts +80 -0
- package/src/codex/repl.ts +50 -0
- package/src/controller.ts +86 -0
- package/src/plugin.ts +90 -0
- package/src/tools/chrome.ts +379 -0
- package/src/tools/computer-use.ts +223 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Locate the Codex install this plugin drives.
|
|
2
|
+
//
|
|
3
|
+
// Nothing here bundles or downloads anything: the `codex` CLI, the shared
|
|
4
|
+
// "Codex Computer Use" app, and the Chrome plugin's bundled browser client are
|
|
5
|
+
// all installed and licensed through the user's own Codex install. This module
|
|
6
|
+
// only READS what is already on disk and reports whether each surface can run.
|
|
7
|
+
|
|
8
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { delimiter, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
const isExecutable = (file: string): boolean => {
|
|
13
|
+
try {
|
|
14
|
+
accessSync(file, constants.X_OK);
|
|
15
|
+
return statSync(file).isFile();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const isReadable = (file: string): boolean => {
|
|
22
|
+
try {
|
|
23
|
+
return statSync(file).isFile();
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const resolveCodexHome = (explicit?: string): string =>
|
|
30
|
+
explicit ?? process.env["CODEX_HOME"] ?? join(homedir(), ".codex");
|
|
31
|
+
|
|
32
|
+
/** The `codex` CLI the bridge spawns. PATH first, then the common install
|
|
33
|
+
* locations for launch contexts that do not inherit the user's shell PATH. */
|
|
34
|
+
export const resolveCodexCli = (explicit?: string): string | undefined => {
|
|
35
|
+
if (explicit !== undefined) return isExecutable(explicit) ? explicit : undefined;
|
|
36
|
+
const dirs = [
|
|
37
|
+
...(process.env["PATH"] ?? "").split(delimiter),
|
|
38
|
+
join(homedir(), ".local", "bin"),
|
|
39
|
+
join(homedir(), ".bun", "bin"),
|
|
40
|
+
"/opt/homebrew/bin",
|
|
41
|
+
"/usr/local/bin",
|
|
42
|
+
];
|
|
43
|
+
for (const dir of dirs) {
|
|
44
|
+
if (dir.length === 0) continue;
|
|
45
|
+
const candidate = join(dir, "codex");
|
|
46
|
+
if (isExecutable(candidate)) return candidate;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The shared Codex Computer Use app: the install marker for Computer Use. */
|
|
52
|
+
const computerUseClientPath = (codexHome: string): string =>
|
|
53
|
+
join(
|
|
54
|
+
codexHome,
|
|
55
|
+
"computer-use",
|
|
56
|
+
"Codex Computer Use.app",
|
|
57
|
+
"Contents",
|
|
58
|
+
"SharedSupport",
|
|
59
|
+
"SkyComputerUseClient.app",
|
|
60
|
+
"Contents",
|
|
61
|
+
"MacOS",
|
|
62
|
+
"SkyComputerUseClient",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
/** The Chrome plugin's bundled browser client, reached through the `latest`
|
|
66
|
+
* symlink Codex maintains beside the versioned directories, so a plugin
|
|
67
|
+
* update does not strand the stored path. */
|
|
68
|
+
export const chromeClientPath = (codexHome: string): string =>
|
|
69
|
+
join(
|
|
70
|
+
codexHome,
|
|
71
|
+
"plugins",
|
|
72
|
+
"cache",
|
|
73
|
+
"openai-bundled",
|
|
74
|
+
"chrome",
|
|
75
|
+
"latest",
|
|
76
|
+
"scripts",
|
|
77
|
+
"browser-client.mjs",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export interface CodexInstall {
|
|
81
|
+
/** Resolved `codex` CLI, or undefined when Codex is not installed. */
|
|
82
|
+
readonly cli?: string;
|
|
83
|
+
readonly codexHome: string;
|
|
84
|
+
readonly computerUse: boolean;
|
|
85
|
+
readonly chrome: boolean;
|
|
86
|
+
/** Absolute path the Chrome surface imports; always present, may not exist. */
|
|
87
|
+
readonly chromeModulePath: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const detectInstall = (options?: {
|
|
91
|
+
readonly codexHome?: string;
|
|
92
|
+
readonly codexCli?: string;
|
|
93
|
+
}): CodexInstall => {
|
|
94
|
+
const codexHome = resolveCodexHome(options?.codexHome);
|
|
95
|
+
const cli = resolveCodexCli(options?.codexCli);
|
|
96
|
+
const chromeModulePath = chromeClientPath(codexHome);
|
|
97
|
+
return {
|
|
98
|
+
...(cli === undefined ? {} : { cli }),
|
|
99
|
+
codexHome,
|
|
100
|
+
computerUse: isExecutable(computerUseClientPath(codexHome)),
|
|
101
|
+
chrome: isReadable(chromeModulePath),
|
|
102
|
+
chromeModulePath,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** The message shown when a surface cannot run yet. Written as ordered steps
|
|
107
|
+
* because the person reading it has just been told they cannot proceed. */
|
|
108
|
+
export const setupHint = (missing: "computer-use" | "chrome" | "codex"): string => {
|
|
109
|
+
if (missing === "codex") {
|
|
110
|
+
return "Codex is not installed. Install the Codex app from openai.com/codex and sign in.";
|
|
111
|
+
}
|
|
112
|
+
if (missing === "chrome") {
|
|
113
|
+
return [
|
|
114
|
+
"The Codex Chrome plugin is not installed.",
|
|
115
|
+
"In Codex, open Settings → Computer use and install the ChatGPT browser extension, then use Chrome once inside Codex so it can reach your browser.",
|
|
116
|
+
].join(" ");
|
|
117
|
+
}
|
|
118
|
+
return [
|
|
119
|
+
"The Codex Computer Use app is not installed.",
|
|
120
|
+
"Install the Codex app from openai.com/codex, open Computer Use once inside Codex, and grant the macOS permissions it asks for.",
|
|
121
|
+
].join(" ");
|
|
122
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Adapted from Executor (MIT, Copyright (c) 2026 Rhys Sullivan). See NOTICE.
|
|
2
|
+
// macOS permission failures for the Codex surfaces.
|
|
3
|
+
//
|
|
4
|
+
// These tools drive the real machine, so macOS gates them behind TCC. The
|
|
5
|
+
// plugin reports a bare Apple Event error code, which reaches a caller as an
|
|
6
|
+
// opaque "Unknown error". Classifying it here turns the most common first-run
|
|
7
|
+
// failure into something a person can act on: which System Settings pane, and
|
|
8
|
+
// which exact entry, to enable.
|
|
9
|
+
|
|
10
|
+
const settingsUrl = (pane: string): string =>
|
|
11
|
+
`x-apple.systempreferences:com.apple.preference.security?${pane}`;
|
|
12
|
+
|
|
13
|
+
export interface CodexPermission {
|
|
14
|
+
readonly id: "automation" | "accessibility" | "screen-recording";
|
|
15
|
+
readonly label: string;
|
|
16
|
+
readonly entry: string;
|
|
17
|
+
readonly why: string;
|
|
18
|
+
readonly settingsUrl: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Automation is per-HOST: macOS attributes the Apple Event to the responsible
|
|
23
|
+
* process — the app that launched the chain, here OpenCode. Screen Recording
|
|
24
|
+
* and Accessibility attach to the Codex Computer Use app and are shared across
|
|
25
|
+
* every host, so granting them once in Codex covers this plugin too.
|
|
26
|
+
*/
|
|
27
|
+
export const CODEX_PERMISSIONS: Readonly<Record<string, readonly CodexPermission[]>> = {
|
|
28
|
+
"computer_use": [
|
|
29
|
+
{
|
|
30
|
+
id: "screen-recording",
|
|
31
|
+
label: "Screen Recording",
|
|
32
|
+
entry: "Codex Computer Use",
|
|
33
|
+
why: "so it can see the app it is operating",
|
|
34
|
+
settingsUrl: settingsUrl("Privacy_ScreenCapture"),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "accessibility",
|
|
38
|
+
label: "Accessibility",
|
|
39
|
+
entry: "Codex Computer Use",
|
|
40
|
+
why: "so it can click, type, and scroll",
|
|
41
|
+
settingsUrl: settingsUrl("Privacy_Accessibility"),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
"browser": [
|
|
45
|
+
{
|
|
46
|
+
id: "automation",
|
|
47
|
+
label: "Automation",
|
|
48
|
+
entry: "OpenCode → Google Chrome",
|
|
49
|
+
why: "so it can drive your browser",
|
|
50
|
+
settingsUrl: settingsUrl("Privacy_Automation"),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Apple Event failures that mean "the user has not allowed this".
|
|
56
|
+
* `-1743` is `errAEEventNotPermitted`; `-600`/`-609` are the connection
|
|
57
|
+
* errors macOS returns when it refuses to hand the sender a port. */
|
|
58
|
+
const deniedCodePattern = /(?:^|[^0-9-])(-1743|-609|-600)(?![0-9])/;
|
|
59
|
+
|
|
60
|
+
/** A permission failure recognised in an upstream tool error, or null when
|
|
61
|
+
* the error is about something else. Matching is on the numeric code, not on
|
|
62
|
+
* wording: the plugin's own text is "Unknown error". */
|
|
63
|
+
export const permissionFailure = (
|
|
64
|
+
message: string,
|
|
65
|
+
surface: string | undefined,
|
|
66
|
+
): CodexPermission | null => {
|
|
67
|
+
if (!deniedCodePattern.test(message)) return null;
|
|
68
|
+
const permissions = surface === undefined ? [] : (CODEX_PERMISSIONS[surface] ?? []);
|
|
69
|
+
return permissions.find((p) => p.id === "automation") ?? permissions[0] ?? null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The message a caller sees instead of "Unknown error". It names the block,
|
|
73
|
+
* the exact entry to enable, and where, because macOS will not ask again on
|
|
74
|
+
* its own after a denial. */
|
|
75
|
+
export const permissionFailureMessage = (permission: CodexPermission): string =>
|
|
76
|
+
[
|
|
77
|
+
`macOS blocked this: ${permission.label} access has not been allowed.`,
|
|
78
|
+
`Open System Settings → Privacy & Security → ${permission.label}, find "${permission.entry}", and turn it on — ${permission.why}.`,
|
|
79
|
+
"macOS only asks once, so a prompt will not appear again until it is enabled there.",
|
|
80
|
+
].join(" ");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Adapted from Executor (MIT, Copyright (c) 2026 Rhys Sullivan). See NOTICE.
|
|
2
|
+
// Shared helpers for projecting Codex's `node_repl` `js` tool as typed tools.
|
|
3
|
+
//
|
|
4
|
+
// Both surfaces (Computer Use via `@oai/sky`, Chrome via `browser-client.mjs`)
|
|
5
|
+
// work the same way: a call is compiled into one JavaScript program, that
|
|
6
|
+
// program is executed through Codex's `node_repl` server, and it reports a
|
|
7
|
+
// single JSON value back through `nodeRepl.write`. The encoding that puts
|
|
8
|
+
// caller arguments INTO that source text and reads one JSON value OUT is the
|
|
9
|
+
// genuinely shared part, and the part that must be exactly right.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An expression that evaluates to `value` inside the REPL.
|
|
13
|
+
*
|
|
14
|
+
* `JSON.parse` of a string, NOT a bare object literal. The two differ in
|
|
15
|
+
* exactly the way that matters here: `__proto__` as a key sets an object's
|
|
16
|
+
* PROTOTYPE in a literal, while being an ordinary own key under `JSON.parse`.
|
|
17
|
+
* Embedding caller arguments as a literal would let a caller move data onto
|
|
18
|
+
* the prototype chain; parsing keeps arguments data.
|
|
19
|
+
*
|
|
20
|
+
* U+2028/U+2029 are legal inside a JSON string but are literal line
|
|
21
|
+
* terminators in a JS source text, so they are escaped.
|
|
22
|
+
*/
|
|
23
|
+
export const jsString = (value: string): string =>
|
|
24
|
+
JSON.stringify(value).replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
25
|
+
|
|
26
|
+
export const jsLiteral = (value: unknown): string =>
|
|
27
|
+
`JSON.parse(${JSON.stringify(JSON.stringify(value ?? null))
|
|
28
|
+
.replaceAll("\u2028", "\\u2028")
|
|
29
|
+
.replaceAll("\u2029", "\\u2029")})`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wrap a program body so it runs in its own scope and reports one JSON value.
|
|
33
|
+
*
|
|
34
|
+
* The scope matters: a REPL session is persistent, so a program that declared
|
|
35
|
+
* its working variables at top level would redeclare the same `const` on every
|
|
36
|
+
* call. Everything per-call lives inside the IIFE; only deliberate caches (the
|
|
37
|
+
* imported runtime) are left on `globalThis`.
|
|
38
|
+
*
|
|
39
|
+
* The REPL returns values only through `nodeRepl.write`, and only as text;
|
|
40
|
+
* `undefined` (every action method) becomes `null` so a caller always gets a
|
|
41
|
+
* well-formed JSON body rather than an empty string.
|
|
42
|
+
*/
|
|
43
|
+
export const writeJsonResult = (body: readonly string[], expression: string): string =>
|
|
44
|
+
[
|
|
45
|
+
"await (async () => {",
|
|
46
|
+
...body.map((line) => ` ${line}`),
|
|
47
|
+
` const result = ${expression};`,
|
|
48
|
+
" nodeRepl.write(JSON.stringify(result ?? null));",
|
|
49
|
+
"})();",
|
|
50
|
+
].join("\n");
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Owns the Codex app-server connection and turns tool calls into results.
|
|
2
|
+
//
|
|
3
|
+
// One connection per plugin instance. `CodexAppServer` serializes calls inside
|
|
4
|
+
// it, so concurrent OpenCode sessions cannot interleave desktop or browser
|
|
5
|
+
// actions. The connection is created lazily on the first tool call and closed
|
|
6
|
+
// when the plugin unloads.
|
|
7
|
+
|
|
8
|
+
import { CodexAppServer, toolCallError } from "./codex/appserver";
|
|
9
|
+
import { detectInstall, setupHint } from "./codex/install";
|
|
10
|
+
import type { ComputerUseTool } from "./tools/computer-use";
|
|
11
|
+
import { computerUseProgram } from "./tools/computer-use";
|
|
12
|
+
import type { ChromeTool } from "./tools/chrome";
|
|
13
|
+
import { chromeProgram } from "./tools/chrome";
|
|
14
|
+
|
|
15
|
+
export const COMPUTER_USE_NAMESPACE = "computer_use";
|
|
16
|
+
export const CHROME_NAMESPACE = "chrome";
|
|
17
|
+
|
|
18
|
+
export interface PluginSettings {
|
|
19
|
+
readonly codexHome?: string;
|
|
20
|
+
readonly codexCli?: string;
|
|
21
|
+
readonly computerUse: boolean;
|
|
22
|
+
readonly chrome: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Controller {
|
|
26
|
+
readonly #settings: PluginSettings;
|
|
27
|
+
#server: CodexAppServer | undefined;
|
|
28
|
+
|
|
29
|
+
constructor(settings: PluginSettings) {
|
|
30
|
+
this.#settings = settings;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#bridge(): CodexAppServer {
|
|
34
|
+
this.#server ??= new CodexAppServer({
|
|
35
|
+
codexCli: this.#settings.codexCli ?? "codex",
|
|
36
|
+
...(this.#settings.codexHome === undefined ? {} : { codexHome: this.#settings.codexHome }),
|
|
37
|
+
onLog: (message) => console.log(`[codex-control] ${message}`),
|
|
38
|
+
});
|
|
39
|
+
return this.#server;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async callComputerUse(tool: ComputerUseTool, input: unknown): Promise<string> {
|
|
43
|
+
const install = detectInstall(this.#settings);
|
|
44
|
+
if (install.cli === undefined) throw new Error(setupHint("codex"));
|
|
45
|
+
if (!install.computerUse) throw new Error(setupHint("computer-use"));
|
|
46
|
+
const result = await this.#bridge().callJs({
|
|
47
|
+
code: computerUseProgram(tool, input),
|
|
48
|
+
title: `Computer Use: ${tool.name}`,
|
|
49
|
+
});
|
|
50
|
+
const error = toolCallError(result, COMPUTER_USE_NAMESPACE);
|
|
51
|
+
if (error !== undefined) throw error;
|
|
52
|
+
return result.text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async callChrome(tool: ChromeTool, input: unknown): Promise<string> {
|
|
56
|
+
const install = detectInstall(this.#settings);
|
|
57
|
+
if (install.cli === undefined) throw new Error(setupHint("codex"));
|
|
58
|
+
if (!install.chrome) throw new Error(setupHint("chrome"));
|
|
59
|
+
const result = await this.#bridge().callJs({
|
|
60
|
+
code: chromeProgram(tool, input, install.chromeModulePath),
|
|
61
|
+
title: `Chrome: ${tool.name}`,
|
|
62
|
+
// Real navigation routinely outruns the REPL's 30s default; a page load
|
|
63
|
+
// plus its DOM pass needs the longer budget.
|
|
64
|
+
timeoutMs: 55_000,
|
|
65
|
+
});
|
|
66
|
+
const error = toolCallError(result, CHROME_NAMESPACE);
|
|
67
|
+
if (error !== undefined) throw error;
|
|
68
|
+
return result.text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async close(): Promise<void> {
|
|
72
|
+
const server = this.#server;
|
|
73
|
+
this.#server = undefined;
|
|
74
|
+
await server?.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const readSettings = (options: unknown): PluginSettings => {
|
|
79
|
+
const o = (options ?? {}) as Record<string, unknown>;
|
|
80
|
+
return {
|
|
81
|
+
...(typeof o["codexHome"] === "string" ? { codexHome: o["codexHome"] } : {}),
|
|
82
|
+
...(typeof o["codexCli"] === "string" ? { codexCli: o["codexCli"] } : {}),
|
|
83
|
+
computerUse: o["computerUse"] !== false,
|
|
84
|
+
chrome: o["chrome"] !== false,
|
|
85
|
+
};
|
|
86
|
+
};
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// OpenCode plugin: Codex Computer Use and Chrome as native tools.
|
|
2
|
+
//
|
|
3
|
+
// Codex's Computer Use and Chrome plugins are driven entirely through
|
|
4
|
+
// `codex app-server` (see `codex/appserver.ts`). This plugin owns one
|
|
5
|
+
// app-server connection, projects each surface as typed tools, and lets
|
|
6
|
+
// OpenCode's permission layer be the consent boundary: every tool carries a
|
|
7
|
+
// `permission` action, and Codex's own approval prompts are answered
|
|
8
|
+
// automatically once the user has allowed the call.
|
|
9
|
+
//
|
|
10
|
+
// Nothing is spawned until the first tool call, and each surface reports a
|
|
11
|
+
// clear setup path when Codex or that surface is not installed.
|
|
12
|
+
|
|
13
|
+
import type { Plugin as PluginNamespace } from "@opencode/plugin";
|
|
14
|
+
|
|
15
|
+
import { resolveCodexHome } from "./codex/install";
|
|
16
|
+
import {
|
|
17
|
+
CHROME_NAMESPACE,
|
|
18
|
+
COMPUTER_USE_NAMESPACE,
|
|
19
|
+
Controller,
|
|
20
|
+
readSettings,
|
|
21
|
+
} from "./controller";
|
|
22
|
+
import { CHROME_TOOLS } from "./tools/chrome";
|
|
23
|
+
import { COMPUTER_USE_TOOLS } from "./tools/computer-use";
|
|
24
|
+
|
|
25
|
+
type PluginContext = PluginNamespace.Context;
|
|
26
|
+
|
|
27
|
+
// A plain object rather than `Plugin.define`: the helper is an identity
|
|
28
|
+
// function, and importing it only for that would pull OpenCode's full server
|
|
29
|
+
// dependency graph into every install. The `satisfies` keeps the type
|
|
30
|
+
// contract without a runtime dependency.
|
|
31
|
+
const plugin = {
|
|
32
|
+
id: "codex-control",
|
|
33
|
+
async setup(ctx: PluginContext) {
|
|
34
|
+
const settings = readSettings(ctx.options);
|
|
35
|
+
const controller = new Controller(settings);
|
|
36
|
+
|
|
37
|
+
await ctx.tool.transform((editor) => {
|
|
38
|
+
if (settings.computerUse) {
|
|
39
|
+
editor.namespace({
|
|
40
|
+
name: COMPUTER_USE_NAMESPACE,
|
|
41
|
+
description:
|
|
42
|
+
"Control macOS desktop apps through Codex Computer Use: read the accessibility tree and screenshots, click, type, and scroll.",
|
|
43
|
+
});
|
|
44
|
+
for (const tool of COMPUTER_USE_TOOLS) {
|
|
45
|
+
editor.add({
|
|
46
|
+
name: tool.name,
|
|
47
|
+
description: tool.description,
|
|
48
|
+
input: tool.inputSchema,
|
|
49
|
+
options: {
|
|
50
|
+
namespace: COMPUTER_USE_NAMESPACE,
|
|
51
|
+
permission: COMPUTER_USE_NAMESPACE,
|
|
52
|
+
},
|
|
53
|
+
execute: async (input: unknown) => ({
|
|
54
|
+
content: await controller.callComputerUse(tool, input),
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (settings.chrome) {
|
|
61
|
+
editor.namespace({
|
|
62
|
+
name: CHROME_NAMESPACE,
|
|
63
|
+
description:
|
|
64
|
+
"Control the real Chrome browser through Codex: open tabs, navigate, read pages, click, and type, using the user's logged-in sessions.",
|
|
65
|
+
});
|
|
66
|
+
for (const tool of CHROME_TOOLS) {
|
|
67
|
+
editor.add({
|
|
68
|
+
name: tool.name,
|
|
69
|
+
description: tool.description,
|
|
70
|
+
input: tool.inputSchema,
|
|
71
|
+
options: { namespace: CHROME_NAMESPACE, permission: CHROME_NAMESPACE },
|
|
72
|
+
execute: async (input: unknown) => ({
|
|
73
|
+
content: await controller.callChrome(tool, input),
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
`[codex-control] loaded (codex home: ${resolveCodexHome(settings.codexHome)}, computer use: ${settings.computerUse}, chrome: ${settings.chrome})`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return async () => {
|
|
85
|
+
await controller.close();
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
} satisfies PluginNamespace.Plugin;
|
|
89
|
+
|
|
90
|
+
export default plugin;
|