open-computer-use 0.1.17 → 0.1.19

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 CHANGED
@@ -62,7 +62,7 @@ open-computer-use install-codex-plugin
62
62
 
63
63
  ## Notes
64
64
 
65
- - Version: `0.1.17`
65
+ - Version: `0.1.19`
66
66
  - Platform: macOS 14+
67
67
  - Architectures: `arm64` and `x64` via a universal app bundle
68
68
  - The host terminal or app still needs macOS `Accessibility` and `Screen Recording` permissions
@@ -21,7 +21,7 @@
21
21
  <key>CFBundlePackageType</key>
22
22
  <string>APPL</string>
23
23
  <key>CFBundleShortVersionString</key>
24
- <string>0.1.17</string>
24
+ <string>0.1.19</string>
25
25
  <key>CFBundleVersion</key>
26
26
  <string>1</string>
27
27
  <key>LSMinimumSystemVersion</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-computer-use",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Prebuilt macOS Computer Use MCP server. After install, run open-computer-use doctor.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/iFurySt/open-codex-computer-use",
@@ -46,6 +46,7 @@
46
46
  "plugins/open-computer-use/assets/",
47
47
  "plugins/open-computer-use/scripts/",
48
48
  "scripts/install-claude-mcp.sh",
49
+ "scripts/install-config-helper.mjs",
49
50
  "scripts/install-codex-mcp.sh",
50
51
  "scripts/install-codex-plugin.sh",
51
52
  "scripts/postinstall.mjs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-computer-use",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Open-source macOS Computer Use MCP server packaged as a Codex plugin.",
5
5
  "author": {
6
6
  "name": "Leo",
@@ -2,6 +2,8 @@
2
2
 
3
3
  set -euo pipefail
4
4
 
5
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ config_helper="${script_dir}/install-config-helper.mjs"
5
7
  claude_config_path="${CLAUDE_CONFIG_PATH:-${HOME}/.claude.json}"
6
8
  project_root="$(pwd -P)"
7
9
  server_name="open-computer-use"
@@ -30,71 +32,4 @@ while [[ $# -gt 0 ]]; do
30
32
  esac
31
33
  done
32
34
 
33
- python3 - "${claude_config_path}" "${project_root}" "${server_name}" "${command_name}" <<'PY'
34
- import json
35
- import sys
36
- from pathlib import Path
37
-
38
- config_path = Path(sys.argv[1])
39
- project_root = sys.argv[2]
40
- server_name = sys.argv[3]
41
- command_name = sys.argv[4]
42
- desired_entry = {
43
- "type": "stdio",
44
- "command": command_name,
45
- "args": ["mcp"],
46
- }
47
- legacy_server_name = "open-codex-computer-use"
48
-
49
- if config_path.exists():
50
- try:
51
- raw = config_path.read_text()
52
- data = json.loads(raw) if raw.strip() else {}
53
- except json.JSONDecodeError as exc:
54
- print(f"Existing Claude config is not valid JSON: {exc}", file=sys.stderr)
55
- sys.exit(1)
56
- else:
57
- data = {}
58
-
59
- if not isinstance(data, dict):
60
- print("Existing Claude config root is not a JSON object; refusing to modify it.", file=sys.stderr)
61
- sys.exit(1)
62
-
63
- projects = data.setdefault("projects", {})
64
- if not isinstance(projects, dict):
65
- print('Existing Claude config has non-object "projects"; refusing to modify it.', file=sys.stderr)
66
- sys.exit(1)
67
-
68
- project_entry = projects.setdefault(project_root, {})
69
- if not isinstance(project_entry, dict):
70
- print(f'Existing Claude project entry for {project_root} is not an object; refusing to modify it.', file=sys.stderr)
71
- sys.exit(1)
72
-
73
- mcp_servers = project_entry.setdefault("mcpServers", {})
74
- if not isinstance(mcp_servers, dict):
75
- print(f'Existing Claude project MCP config for {project_root} is not an object; refusing to modify it.', file=sys.stderr)
76
- sys.exit(1)
77
-
78
- target = mcp_servers.get(server_name)
79
- legacy = mcp_servers.get(legacy_server_name)
80
-
81
- target_matches = target == desired_entry
82
- legacy_matches = legacy == desired_entry
83
-
84
- if target_matches and not legacy_matches:
85
- print(f'Claude MCP server "{server_name}" is already installed for {project_root} in {config_path}.')
86
- sys.exit(0)
87
-
88
- mcp_servers[server_name] = desired_entry
89
-
90
- if legacy_matches:
91
- del mcp_servers[legacy_server_name]
92
-
93
- config_path.parent.mkdir(parents=True, exist_ok=True)
94
- config_path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
95
-
96
- if target_matches and legacy_matches:
97
- print(f'Claude MCP server "{server_name}" was already installed for {project_root}; removed legacy alias "{legacy_server_name}" from {config_path}.')
98
- else:
99
- print(f'Installed Claude MCP server "{server_name}" for {project_root} into {config_path}.')
100
- PY
35
+ node "${config_helper}" claude-mcp "${claude_config_path}" "${project_root}" "${server_name}" "${command_name}"
@@ -2,6 +2,8 @@
2
2
 
3
3
  set -euo pipefail
4
4
 
5
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ config_helper="${script_dir}/install-config-helper.mjs"
5
7
  codex_home="${CODEX_HOME:-${HOME}/.codex}"
6
8
  config_path="${codex_home}/config.toml"
7
9
  server_name="open-computer-use"
@@ -32,88 +34,4 @@ done
32
34
 
33
35
  mkdir -p "${codex_home}"
34
36
 
35
- python3 - "${config_path}" "${server_name}" "${command_name}" <<'PY'
36
- import json
37
- import re
38
- import sys
39
- from pathlib import Path
40
-
41
- try:
42
- import tomllib
43
- except ModuleNotFoundError as exc:
44
- print(f"python3 with tomllib is required: {exc}", file=sys.stderr)
45
- sys.exit(1)
46
-
47
-
48
- def section_pattern(header: str) -> re.Pattern[str]:
49
- return re.compile(rf'(?ms)^\[{re.escape(header)}\]\n.*?(?=^\[|\Z)')
50
-
51
-
52
- def remove_section(text: str, header: str) -> str:
53
- return section_pattern(header).sub("", text)
54
-
55
-
56
- def upsert_section(text: str, header: str, body: str) -> str:
57
- section = f'[{header}]\n{body.rstrip()}\n'
58
- pattern = section_pattern(header)
59
- if pattern.search(text):
60
- return pattern.sub(section, text, count=1)
61
-
62
- if text and not text.endswith("\n"):
63
- text += "\n"
64
- if text and not text.endswith("\n\n"):
65
- text += "\n"
66
- return text + section
67
-
68
-
69
- config_path = Path(sys.argv[1])
70
- server_name = sys.argv[2]
71
- command_name = sys.argv[3]
72
- desired_args = ["mcp"]
73
- legacy_server_name = "open-codex-computer-use"
74
-
75
- text = config_path.read_text() if config_path.exists() else ""
76
-
77
- try:
78
- parsed = tomllib.loads(text) if text.strip() else {}
79
- except tomllib.TOMLDecodeError as exc:
80
- print(f"Existing Codex config is not valid TOML: {exc}", file=sys.stderr)
81
- sys.exit(1)
82
-
83
- mcp_servers = parsed.get("mcp_servers")
84
- if mcp_servers is not None and not isinstance(mcp_servers, dict):
85
- print('Existing Codex config has non-table "mcp_servers"; refusing to modify it.', file=sys.stderr)
86
- sys.exit(1)
87
-
88
- target = (mcp_servers or {}).get(server_name)
89
- legacy = (mcp_servers or {}).get(legacy_server_name)
90
-
91
- target_matches = (
92
- isinstance(target, dict)
93
- and target.get("command") == command_name
94
- and target.get("args") == desired_args
95
- )
96
- legacy_matches = (
97
- isinstance(legacy, dict)
98
- and legacy.get("command") == command_name
99
- and legacy.get("args") == desired_args
100
- )
101
-
102
- if target_matches and not legacy_matches:
103
- print(f'Codex MCP server "{server_name}" is already installed in {config_path}.')
104
- sys.exit(0)
105
-
106
- body = f'command = {json.dumps(command_name)}\nargs = {json.dumps(desired_args)}'
107
- text = upsert_section(text, f'mcp_servers."{server_name}"', body)
108
-
109
- if legacy_matches:
110
- text = remove_section(text, f'mcp_servers."{legacy_server_name}"')
111
-
112
- text = re.sub(r"\n{3,}", "\n\n", text).rstrip() + "\n"
113
- config_path.write_text(text)
114
-
115
- if target_matches and legacy_matches:
116
- print(f'Codex MCP server "{server_name}" was already installed; removed legacy alias "{legacy_server_name}" from {config_path}.')
117
- else:
118
- print(f'Installed Codex MCP server "{server_name}" into {config_path}.')
119
- PY
37
+ node "${config_helper}" codex-mcp "${config_path}" "${server_name}" "${command_name}"
@@ -3,6 +3,7 @@
3
3
  set -euo pipefail
4
4
 
5
5
  repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ config_helper="${repo_root}/scripts/install-config-helper.mjs"
6
7
  codex_home="${CODEX_HOME:-${HOME}/.codex}"
7
8
  config_path="${codex_home}/config.toml"
8
9
  marketplace_name="open-computer-use-local"
@@ -84,17 +85,7 @@ if [[ -z "${app_bundle}" || ! -x "${app_binary}" ]]; then
84
85
  exit 1
85
86
  fi
86
87
 
87
- plugin_version="$(
88
- python3 - "${plugin_manifest}" <<'PY'
89
- import json
90
- import sys
91
-
92
- with open(sys.argv[1], "r", encoding="utf-8") as fh:
93
- manifest = json.load(fh)
94
-
95
- print(manifest["version"])
96
- PY
97
- )"
88
+ plugin_version="$(node "${config_helper}" codex-plugin-version "${plugin_manifest}")"
98
89
 
99
90
  if [[ -z "${plugin_version}" ]]; then
100
91
  echo "Failed to read plugin version from ${plugin_manifest}" >&2
@@ -111,61 +102,7 @@ mkdir -p "${plugin_install_root}"
111
102
  rsync -a "${plugin_source_root}/" "${plugin_install_root}/"
112
103
  rsync -a "${app_bundle}" "${plugin_install_root}/"
113
104
 
114
- python3 - "${config_path}" "${repo_root}" "${marketplace_name}" "${plugin_name}" <<'PY'
115
- import json
116
- import re
117
- import sys
118
- from pathlib import Path
119
-
120
-
121
- def section_pattern(header: str) -> re.Pattern[str]:
122
- return re.compile(rf'(?ms)^\[{re.escape(header)}\]\n.*?(?=^\[|\Z)')
123
-
124
-
125
- def remove_section(text: str, header: str) -> str:
126
- return section_pattern(header).sub("", text)
127
-
128
-
129
- def upsert_section(text: str, header: str, body: str) -> str:
130
- section = f'[{header}]\n{body.rstrip()}\n'
131
- pattern = section_pattern(header)
132
- if pattern.search(text):
133
- return pattern.sub(section, text, count=1)
134
-
135
- if text and not text.endswith("\n"):
136
- text += "\n"
137
- if text and not text.endswith("\n\n"):
138
- text += "\n"
139
- return text + section
140
-
141
-
142
- config_path = Path(sys.argv[1])
143
- repo_root = Path(sys.argv[2]).resolve()
144
- marketplace_name = sys.argv[3]
145
- plugin_name = sys.argv[4]
146
-
147
- text = config_path.read_text() if config_path.exists() else ""
148
-
149
- for header in (
150
- 'mcp_servers."open-codex-computer-use"',
151
- 'mcp_servers."open-computer-use"',
152
- ):
153
- text = remove_section(text, header)
154
-
155
- text = upsert_section(
156
- text,
157
- f"marketplaces.{marketplace_name}",
158
- f'source_type = "local"\nsource = {json.dumps(str(repo_root))}',
159
- )
160
- text = upsert_section(
161
- text,
162
- f'plugins."{plugin_name}@{marketplace_name}"',
163
- "enabled = true",
164
- )
165
-
166
- text = re.sub(r"\n{3,}", "\n\n", text).rstrip() + "\n"
167
- config_path.write_text(text)
168
- PY
105
+ node "${config_helper}" codex-plugin-config "${config_path}" "${repo_root}" "${marketplace_name}" "${plugin_name}"
169
106
 
170
107
  echo "Installed ${plugin_name}@${marketplace_name}"
171
108
  echo "Marketplace source: ${repo_root}"
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+
6
+ function fail(message) {
7
+ process.stderr.write(`${message}\n`);
8
+ process.exit(1);
9
+ }
10
+
11
+ function usage() {
12
+ process.stdout.write(`Usage:
13
+ node ./scripts/install-config-helper.mjs claude-mcp <config-path> <project-root> <server-name> <command-name>
14
+ node ./scripts/install-config-helper.mjs codex-mcp <config-path> <server-name> <command-name>
15
+ node ./scripts/install-config-helper.mjs codex-plugin-version <plugin-manifest-path>
16
+ node ./scripts/install-config-helper.mjs codex-plugin-config <config-path> <repo-root> <marketplace-name> <plugin-name>
17
+ `);
18
+ }
19
+
20
+ function readTextIfExists(filePath) {
21
+ if (!existsSync(filePath)) {
22
+ return "";
23
+ }
24
+ return readFileSync(filePath, "utf8");
25
+ }
26
+
27
+ function ensureParentDir(filePath) {
28
+ mkdirSync(path.dirname(filePath), { recursive: true });
29
+ }
30
+
31
+ function normalizeNewlines(text) {
32
+ return text.replace(/\r\n/g, "\n");
33
+ }
34
+
35
+ function trimTrailingBlankLines(lines) {
36
+ let end = lines.length;
37
+ while (end > 0 && lines[end - 1].trim() === "") {
38
+ end -= 1;
39
+ }
40
+ return lines.slice(0, end);
41
+ }
42
+
43
+ function canonicalSectionBody(bodyLines) {
44
+ const lines = [...bodyLines];
45
+ while (lines.length > 0 && lines[0].trim() === "") {
46
+ lines.shift();
47
+ }
48
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
49
+ lines.pop();
50
+ }
51
+ return lines.join("\n");
52
+ }
53
+
54
+ function splitTomlSections(text) {
55
+ const normalized = normalizeNewlines(text);
56
+ if (normalized.length === 0) {
57
+ return { preambleLines: [], sections: [] };
58
+ }
59
+
60
+ const lines = normalized.split("\n");
61
+ const preambleLines = [];
62
+ const sections = [];
63
+ let currentHeader = null;
64
+ let currentBodyLines = [];
65
+
66
+ for (const line of lines) {
67
+ const headerMatch = line.match(/^\[([^\]]+)\]\s*$/);
68
+ if (headerMatch) {
69
+ if (currentHeader === null) {
70
+ preambleLines.push(...currentBodyLines);
71
+ } else {
72
+ sections.push({ header: currentHeader, bodyLines: currentBodyLines });
73
+ }
74
+ currentHeader = headerMatch[1];
75
+ currentBodyLines = [];
76
+ continue;
77
+ }
78
+ currentBodyLines.push(line);
79
+ }
80
+
81
+ if (currentHeader === null) {
82
+ preambleLines.push(...currentBodyLines);
83
+ } else {
84
+ sections.push({ header: currentHeader, bodyLines: currentBodyLines });
85
+ }
86
+
87
+ return { preambleLines, sections };
88
+ }
89
+
90
+ function renderTomlDocument(document) {
91
+ const chunks = [];
92
+ const preamble = trimTrailingBlankLines(document.preambleLines);
93
+ if (preamble.length > 0) {
94
+ chunks.push(preamble.join("\n"));
95
+ }
96
+
97
+ for (const section of document.sections) {
98
+ const bodyLines = trimTrailingBlankLines(section.bodyLines);
99
+ if (bodyLines.length > 0) {
100
+ chunks.push(`[${section.header}]\n${bodyLines.join("\n")}`);
101
+ } else {
102
+ chunks.push(`[${section.header}]`);
103
+ }
104
+ }
105
+
106
+ return chunks.length > 0 ? `${chunks.join("\n\n")}\n` : "";
107
+ }
108
+
109
+ function ensureUniqueManagedHeaders(document, headers, configPath) {
110
+ for (const header of headers) {
111
+ const count = document.sections.filter((section) => section.header === header).length;
112
+ if (count > 1) {
113
+ fail(`Existing Codex config has duplicate section [${header}] in ${configPath}; refusing to modify it.`);
114
+ }
115
+ }
116
+ }
117
+
118
+ function applyTomlSectionUpdates(text, updates, configPath) {
119
+ const document = splitTomlSections(text);
120
+ const managedHeaders = [
121
+ ...updates.removeHeaders,
122
+ ...updates.upserts.map((entry) => entry.header),
123
+ ];
124
+ ensureUniqueManagedHeaders(document, managedHeaders, configPath);
125
+
126
+ const upsertMap = new Map(
127
+ updates.upserts.map((entry) => [
128
+ entry.header,
129
+ {
130
+ header: entry.header,
131
+ bodyLines: normalizeNewlines(entry.body).split("\n"),
132
+ },
133
+ ]),
134
+ );
135
+ const removeSet = new Set(updates.removeHeaders);
136
+ const nextSections = [];
137
+ const insertedHeaders = new Set();
138
+
139
+ for (const section of document.sections) {
140
+ if (removeSet.has(section.header)) {
141
+ continue;
142
+ }
143
+ if (upsertMap.has(section.header)) {
144
+ nextSections.push(upsertMap.get(section.header));
145
+ insertedHeaders.add(section.header);
146
+ continue;
147
+ }
148
+ nextSections.push(section);
149
+ }
150
+
151
+ for (const entry of updates.upserts) {
152
+ if (!insertedHeaders.has(entry.header)) {
153
+ nextSections.push(upsertMap.get(entry.header));
154
+ }
155
+ }
156
+
157
+ return renderTomlDocument({
158
+ preambleLines: document.preambleLines,
159
+ sections: nextSections,
160
+ });
161
+ }
162
+
163
+ function installClaudeMcp(configPath, projectRoot, serverName, commandName) {
164
+ const desiredEntry = {
165
+ type: "stdio",
166
+ command: commandName,
167
+ args: ["mcp"],
168
+ };
169
+ const legacyServerName = "open-codex-computer-use";
170
+
171
+ const raw = readTextIfExists(configPath);
172
+ let data;
173
+
174
+ if (raw.trim().length === 0) {
175
+ data = {};
176
+ } else {
177
+ try {
178
+ data = JSON.parse(raw);
179
+ } catch (error) {
180
+ fail(`Existing Claude config is not valid JSON: ${error.message}`);
181
+ }
182
+ }
183
+
184
+ if (data === null || Array.isArray(data) || typeof data !== "object") {
185
+ fail("Existing Claude config root is not a JSON object; refusing to modify it.");
186
+ }
187
+
188
+ const projects = data.projects ?? {};
189
+ if (projects === null || Array.isArray(projects) || typeof projects !== "object") {
190
+ fail('Existing Claude config has non-object "projects"; refusing to modify it.');
191
+ }
192
+ data.projects = projects;
193
+
194
+ const projectEntry = projects[projectRoot] ?? {};
195
+ if (projectEntry === null || Array.isArray(projectEntry) || typeof projectEntry !== "object") {
196
+ fail(`Existing Claude project entry for ${projectRoot} is not an object; refusing to modify it.`);
197
+ }
198
+ projects[projectRoot] = projectEntry;
199
+
200
+ const mcpServers = projectEntry.mcpServers ?? {};
201
+ if (mcpServers === null || Array.isArray(mcpServers) || typeof mcpServers !== "object") {
202
+ fail(`Existing Claude project MCP config for ${projectRoot} is not an object; refusing to modify it.`);
203
+ }
204
+ projectEntry.mcpServers = mcpServers;
205
+
206
+ const target = mcpServers[serverName];
207
+ const legacy = mcpServers[legacyServerName];
208
+ const targetMatches = JSON.stringify(target) === JSON.stringify(desiredEntry);
209
+ const legacyMatches = JSON.stringify(legacy) === JSON.stringify(desiredEntry);
210
+
211
+ if (targetMatches && !legacyMatches) {
212
+ process.stdout.write(`Claude MCP server "${serverName}" is already installed for ${projectRoot} in ${configPath}.\n`);
213
+ return;
214
+ }
215
+
216
+ mcpServers[serverName] = desiredEntry;
217
+ if (legacyMatches) {
218
+ delete mcpServers[legacyServerName];
219
+ }
220
+
221
+ ensureParentDir(configPath);
222
+ writeFileSync(configPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
223
+
224
+ if (targetMatches && legacyMatches) {
225
+ process.stdout.write(`Claude MCP server "${serverName}" was already installed for ${projectRoot}; removed legacy alias "${legacyServerName}" from ${configPath}.\n`);
226
+ } else {
227
+ process.stdout.write(`Installed Claude MCP server "${serverName}" for ${projectRoot} into ${configPath}.\n`);
228
+ }
229
+ }
230
+
231
+ function installCodexMcp(configPath, serverName, commandName) {
232
+ const desiredBody = `command = ${JSON.stringify(commandName)}\nargs = ["mcp"]`;
233
+ const targetHeader = `mcp_servers."${serverName}"`;
234
+ const legacyServerName = "open-codex-computer-use";
235
+ const legacyHeader = `mcp_servers."${legacyServerName}"`;
236
+ const text = readTextIfExists(configPath);
237
+ const document = splitTomlSections(text);
238
+
239
+ ensureUniqueManagedHeaders(document, [targetHeader, legacyHeader], configPath);
240
+
241
+ const targetSection = document.sections.find((section) => section.header === targetHeader);
242
+ const legacySection = document.sections.find((section) => section.header === legacyHeader);
243
+ const desiredCanonical = canonicalSectionBody(desiredBody.split("\n"));
244
+ const targetMatches = targetSection ? canonicalSectionBody(targetSection.bodyLines) === desiredCanonical : false;
245
+ const legacyMatches = legacySection ? canonicalSectionBody(legacySection.bodyLines) === desiredCanonical : false;
246
+
247
+ if (targetMatches && !legacyMatches) {
248
+ process.stdout.write(`Codex MCP server "${serverName}" is already installed in ${configPath}.\n`);
249
+ return;
250
+ }
251
+
252
+ const nextText = applyTomlSectionUpdates(
253
+ text,
254
+ {
255
+ removeHeaders: [legacyHeader],
256
+ upserts: [{ header: targetHeader, body: desiredBody }],
257
+ },
258
+ configPath,
259
+ );
260
+
261
+ ensureParentDir(configPath);
262
+ writeFileSync(configPath, nextText, "utf8");
263
+
264
+ if (targetMatches && legacyMatches) {
265
+ process.stdout.write(`Codex MCP server "${serverName}" was already installed; removed legacy alias "${legacyServerName}" from ${configPath}.\n`);
266
+ } else {
267
+ process.stdout.write(`Installed Codex MCP server "${serverName}" into ${configPath}.\n`);
268
+ }
269
+ }
270
+
271
+ function printCodexPluginVersion(pluginManifestPath) {
272
+ let manifest;
273
+ try {
274
+ manifest = JSON.parse(readFileSync(pluginManifestPath, "utf8"));
275
+ } catch (error) {
276
+ fail(`Failed to read plugin manifest ${pluginManifestPath}: ${error.message}`);
277
+ }
278
+
279
+ if (!manifest || typeof manifest.version !== "string" || manifest.version.length === 0) {
280
+ fail(`Plugin manifest ${pluginManifestPath} does not contain a valid string "version".`);
281
+ }
282
+
283
+ process.stdout.write(`${manifest.version}\n`);
284
+ }
285
+
286
+ function installCodexPluginConfig(configPath, repoRoot, marketplaceName, pluginName) {
287
+ const text = readTextIfExists(configPath);
288
+ const repoRootPath = path.resolve(repoRoot);
289
+ const nextText = applyTomlSectionUpdates(
290
+ text,
291
+ {
292
+ removeHeaders: [
293
+ 'mcp_servers."open-codex-computer-use"',
294
+ 'mcp_servers."open-computer-use"',
295
+ ],
296
+ upserts: [
297
+ {
298
+ header: `marketplaces.${marketplaceName}`,
299
+ body: `source_type = "local"\nsource = ${JSON.stringify(repoRootPath)}`,
300
+ },
301
+ {
302
+ header: `plugins."${pluginName}@${marketplaceName}"`,
303
+ body: "enabled = true",
304
+ },
305
+ ],
306
+ },
307
+ configPath,
308
+ );
309
+
310
+ ensureParentDir(configPath);
311
+ writeFileSync(configPath, nextText, "utf8");
312
+ }
313
+
314
+ function main(argv) {
315
+ const [command, ...args] = argv;
316
+ switch (command) {
317
+ case "claude-mcp":
318
+ if (args.length !== 4) {
319
+ usage();
320
+ process.exit(1);
321
+ }
322
+ installClaudeMcp(...args);
323
+ return;
324
+ case "codex-mcp":
325
+ if (args.length !== 3) {
326
+ usage();
327
+ process.exit(1);
328
+ }
329
+ installCodexMcp(...args);
330
+ return;
331
+ case "codex-plugin-version":
332
+ if (args.length !== 1) {
333
+ usage();
334
+ process.exit(1);
335
+ }
336
+ printCodexPluginVersion(args[0]);
337
+ return;
338
+ case "codex-plugin-config":
339
+ if (args.length !== 4) {
340
+ usage();
341
+ process.exit(1);
342
+ }
343
+ installCodexPluginConfig(...args);
344
+ return;
345
+ default:
346
+ usage();
347
+ process.exit(1);
348
+ }
349
+ }
350
+
351
+ main(process.argv.slice(2));
@@ -11,7 +11,7 @@ const mcpConfig = {
11
11
  };
12
12
  const lines = [
13
13
  "",
14
- "Installed open-computer-use@0.1.17.",
14
+ "Installed open-computer-use@0.1.19.",
15
15
  "Package: https://www.npmjs.com/package/open-computer-use",
16
16
  "Commands: open-computer-use, open-computer-use-mcp, open-codex-computer-use-mcp",
17
17
  "",