open-computer-use 0.1.18 → 0.1.20
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 +1 -1
- package/dist/Open Computer Use.app/Contents/Info.plist +1 -1
- package/dist/Open Computer Use.app/Contents/MacOS/OpenComputerUse +0 -0
- package/package.json +2 -1
- package/plugins/open-computer-use/.codex-plugin/plugin.json +1 -1
- package/scripts/install-claude-mcp.sh +3 -68
- package/scripts/install-codex-mcp.sh +3 -85
- package/scripts/install-codex-plugin.sh +4 -68
- package/scripts/install-config-helper.mjs +377 -0
- package/scripts/postinstall.mjs +1 -1
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.
|
|
65
|
+
- Version: `0.1.20`
|
|
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
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-computer-use",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -108,64 +99,9 @@ mkdir -p "${codex_home}" "${plugin_cache_root}"
|
|
|
108
99
|
rm -rf "${plugin_install_root}"
|
|
109
100
|
mkdir -p "${plugin_install_root}"
|
|
110
101
|
|
|
111
|
-
|
|
112
|
-
rsync -a "${app_bundle}" "${plugin_install_root}/"
|
|
113
|
-
|
|
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
|
-
)
|
|
102
|
+
node "${config_helper}" copy-into-dir "${plugin_install_root}" "${plugin_source_root}" "${app_bundle}"
|
|
165
103
|
|
|
166
|
-
|
|
167
|
-
config_path.write_text(text)
|
|
168
|
-
PY
|
|
104
|
+
node "${config_helper}" codex-plugin-config "${config_path}" "${repo_root}" "${marketplace_name}" "${plugin_name}"
|
|
169
105
|
|
|
170
106
|
echo "Installed ${plugin_name}@${marketplace_name}"
|
|
171
107
|
echo "Marketplace source: ${repo_root}"
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, 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
|
+
node ./scripts/install-config-helper.mjs copy-into-dir <target-dir> <source-path> [<source-path> ...]
|
|
18
|
+
`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readTextIfExists(filePath) {
|
|
22
|
+
if (!existsSync(filePath)) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
return readFileSync(filePath, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ensureParentDir(filePath) {
|
|
29
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeNewlines(text) {
|
|
33
|
+
return text.replace(/\r\n/g, "\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function trimTrailingBlankLines(lines) {
|
|
37
|
+
let end = lines.length;
|
|
38
|
+
while (end > 0 && lines[end - 1].trim() === "") {
|
|
39
|
+
end -= 1;
|
|
40
|
+
}
|
|
41
|
+
return lines.slice(0, end);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonicalSectionBody(bodyLines) {
|
|
45
|
+
const lines = [...bodyLines];
|
|
46
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
47
|
+
lines.shift();
|
|
48
|
+
}
|
|
49
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
50
|
+
lines.pop();
|
|
51
|
+
}
|
|
52
|
+
return lines.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function splitTomlSections(text) {
|
|
56
|
+
const normalized = normalizeNewlines(text);
|
|
57
|
+
if (normalized.length === 0) {
|
|
58
|
+
return { preambleLines: [], sections: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const lines = normalized.split("\n");
|
|
62
|
+
const preambleLines = [];
|
|
63
|
+
const sections = [];
|
|
64
|
+
let currentHeader = null;
|
|
65
|
+
let currentBodyLines = [];
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const headerMatch = line.match(/^\[([^\]]+)\]\s*$/);
|
|
69
|
+
if (headerMatch) {
|
|
70
|
+
if (currentHeader === null) {
|
|
71
|
+
preambleLines.push(...currentBodyLines);
|
|
72
|
+
} else {
|
|
73
|
+
sections.push({ header: currentHeader, bodyLines: currentBodyLines });
|
|
74
|
+
}
|
|
75
|
+
currentHeader = headerMatch[1];
|
|
76
|
+
currentBodyLines = [];
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
currentBodyLines.push(line);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (currentHeader === null) {
|
|
83
|
+
preambleLines.push(...currentBodyLines);
|
|
84
|
+
} else {
|
|
85
|
+
sections.push({ header: currentHeader, bodyLines: currentBodyLines });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { preambleLines, sections };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderTomlDocument(document) {
|
|
92
|
+
const chunks = [];
|
|
93
|
+
const preamble = trimTrailingBlankLines(document.preambleLines);
|
|
94
|
+
if (preamble.length > 0) {
|
|
95
|
+
chunks.push(preamble.join("\n"));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const section of document.sections) {
|
|
99
|
+
const bodyLines = trimTrailingBlankLines(section.bodyLines);
|
|
100
|
+
if (bodyLines.length > 0) {
|
|
101
|
+
chunks.push(`[${section.header}]\n${bodyLines.join("\n")}`);
|
|
102
|
+
} else {
|
|
103
|
+
chunks.push(`[${section.header}]`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return chunks.length > 0 ? `${chunks.join("\n\n")}\n` : "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ensureUniqueManagedHeaders(document, headers, configPath) {
|
|
111
|
+
for (const header of headers) {
|
|
112
|
+
const count = document.sections.filter((section) => section.header === header).length;
|
|
113
|
+
if (count > 1) {
|
|
114
|
+
fail(`Existing Codex config has duplicate section [${header}] in ${configPath}; refusing to modify it.`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function applyTomlSectionUpdates(text, updates, configPath) {
|
|
120
|
+
const document = splitTomlSections(text);
|
|
121
|
+
const managedHeaders = [
|
|
122
|
+
...updates.removeHeaders,
|
|
123
|
+
...updates.upserts.map((entry) => entry.header),
|
|
124
|
+
];
|
|
125
|
+
ensureUniqueManagedHeaders(document, managedHeaders, configPath);
|
|
126
|
+
|
|
127
|
+
const upsertMap = new Map(
|
|
128
|
+
updates.upserts.map((entry) => [
|
|
129
|
+
entry.header,
|
|
130
|
+
{
|
|
131
|
+
header: entry.header,
|
|
132
|
+
bodyLines: normalizeNewlines(entry.body).split("\n"),
|
|
133
|
+
},
|
|
134
|
+
]),
|
|
135
|
+
);
|
|
136
|
+
const removeSet = new Set(updates.removeHeaders);
|
|
137
|
+
const nextSections = [];
|
|
138
|
+
const insertedHeaders = new Set();
|
|
139
|
+
|
|
140
|
+
for (const section of document.sections) {
|
|
141
|
+
if (removeSet.has(section.header)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (upsertMap.has(section.header)) {
|
|
145
|
+
nextSections.push(upsertMap.get(section.header));
|
|
146
|
+
insertedHeaders.add(section.header);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
nextSections.push(section);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const entry of updates.upserts) {
|
|
153
|
+
if (!insertedHeaders.has(entry.header)) {
|
|
154
|
+
nextSections.push(upsertMap.get(entry.header));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return renderTomlDocument({
|
|
159
|
+
preambleLines: document.preambleLines,
|
|
160
|
+
sections: nextSections,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function installClaudeMcp(configPath, projectRoot, serverName, commandName) {
|
|
165
|
+
const desiredEntry = {
|
|
166
|
+
type: "stdio",
|
|
167
|
+
command: commandName,
|
|
168
|
+
args: ["mcp"],
|
|
169
|
+
};
|
|
170
|
+
const legacyServerName = "open-codex-computer-use";
|
|
171
|
+
|
|
172
|
+
const raw = readTextIfExists(configPath);
|
|
173
|
+
let data;
|
|
174
|
+
|
|
175
|
+
if (raw.trim().length === 0) {
|
|
176
|
+
data = {};
|
|
177
|
+
} else {
|
|
178
|
+
try {
|
|
179
|
+
data = JSON.parse(raw);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
fail(`Existing Claude config is not valid JSON: ${error.message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (data === null || Array.isArray(data) || typeof data !== "object") {
|
|
186
|
+
fail("Existing Claude config root is not a JSON object; refusing to modify it.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const projects = data.projects ?? {};
|
|
190
|
+
if (projects === null || Array.isArray(projects) || typeof projects !== "object") {
|
|
191
|
+
fail('Existing Claude config has non-object "projects"; refusing to modify it.');
|
|
192
|
+
}
|
|
193
|
+
data.projects = projects;
|
|
194
|
+
|
|
195
|
+
const projectEntry = projects[projectRoot] ?? {};
|
|
196
|
+
if (projectEntry === null || Array.isArray(projectEntry) || typeof projectEntry !== "object") {
|
|
197
|
+
fail(`Existing Claude project entry for ${projectRoot} is not an object; refusing to modify it.`);
|
|
198
|
+
}
|
|
199
|
+
projects[projectRoot] = projectEntry;
|
|
200
|
+
|
|
201
|
+
const mcpServers = projectEntry.mcpServers ?? {};
|
|
202
|
+
if (mcpServers === null || Array.isArray(mcpServers) || typeof mcpServers !== "object") {
|
|
203
|
+
fail(`Existing Claude project MCP config for ${projectRoot} is not an object; refusing to modify it.`);
|
|
204
|
+
}
|
|
205
|
+
projectEntry.mcpServers = mcpServers;
|
|
206
|
+
|
|
207
|
+
const target = mcpServers[serverName];
|
|
208
|
+
const legacy = mcpServers[legacyServerName];
|
|
209
|
+
const targetMatches = JSON.stringify(target) === JSON.stringify(desiredEntry);
|
|
210
|
+
const legacyMatches = JSON.stringify(legacy) === JSON.stringify(desiredEntry);
|
|
211
|
+
|
|
212
|
+
if (targetMatches && !legacyMatches) {
|
|
213
|
+
process.stdout.write(`Claude MCP server "${serverName}" is already installed for ${projectRoot} in ${configPath}.\n`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
mcpServers[serverName] = desiredEntry;
|
|
218
|
+
if (legacyMatches) {
|
|
219
|
+
delete mcpServers[legacyServerName];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
ensureParentDir(configPath);
|
|
223
|
+
writeFileSync(configPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
224
|
+
|
|
225
|
+
if (targetMatches && legacyMatches) {
|
|
226
|
+
process.stdout.write(`Claude MCP server "${serverName}" was already installed for ${projectRoot}; removed legacy alias "${legacyServerName}" from ${configPath}.\n`);
|
|
227
|
+
} else {
|
|
228
|
+
process.stdout.write(`Installed Claude MCP server "${serverName}" for ${projectRoot} into ${configPath}.\n`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function installCodexMcp(configPath, serverName, commandName) {
|
|
233
|
+
const desiredBody = `command = ${JSON.stringify(commandName)}\nargs = ["mcp"]`;
|
|
234
|
+
const targetHeader = `mcp_servers."${serverName}"`;
|
|
235
|
+
const legacyServerName = "open-codex-computer-use";
|
|
236
|
+
const legacyHeader = `mcp_servers."${legacyServerName}"`;
|
|
237
|
+
const text = readTextIfExists(configPath);
|
|
238
|
+
const document = splitTomlSections(text);
|
|
239
|
+
|
|
240
|
+
ensureUniqueManagedHeaders(document, [targetHeader, legacyHeader], configPath);
|
|
241
|
+
|
|
242
|
+
const targetSection = document.sections.find((section) => section.header === targetHeader);
|
|
243
|
+
const legacySection = document.sections.find((section) => section.header === legacyHeader);
|
|
244
|
+
const desiredCanonical = canonicalSectionBody(desiredBody.split("\n"));
|
|
245
|
+
const targetMatches = targetSection ? canonicalSectionBody(targetSection.bodyLines) === desiredCanonical : false;
|
|
246
|
+
const legacyMatches = legacySection ? canonicalSectionBody(legacySection.bodyLines) === desiredCanonical : false;
|
|
247
|
+
|
|
248
|
+
if (targetMatches && !legacyMatches) {
|
|
249
|
+
process.stdout.write(`Codex MCP server "${serverName}" is already installed in ${configPath}.\n`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const nextText = applyTomlSectionUpdates(
|
|
254
|
+
text,
|
|
255
|
+
{
|
|
256
|
+
removeHeaders: [legacyHeader],
|
|
257
|
+
upserts: [{ header: targetHeader, body: desiredBody }],
|
|
258
|
+
},
|
|
259
|
+
configPath,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
ensureParentDir(configPath);
|
|
263
|
+
writeFileSync(configPath, nextText, "utf8");
|
|
264
|
+
|
|
265
|
+
if (targetMatches && legacyMatches) {
|
|
266
|
+
process.stdout.write(`Codex MCP server "${serverName}" was already installed; removed legacy alias "${legacyServerName}" from ${configPath}.\n`);
|
|
267
|
+
} else {
|
|
268
|
+
process.stdout.write(`Installed Codex MCP server "${serverName}" into ${configPath}.\n`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function printCodexPluginVersion(pluginManifestPath) {
|
|
273
|
+
let manifest;
|
|
274
|
+
try {
|
|
275
|
+
manifest = JSON.parse(readFileSync(pluginManifestPath, "utf8"));
|
|
276
|
+
} catch (error) {
|
|
277
|
+
fail(`Failed to read plugin manifest ${pluginManifestPath}: ${error.message}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!manifest || typeof manifest.version !== "string" || manifest.version.length === 0) {
|
|
281
|
+
fail(`Plugin manifest ${pluginManifestPath} does not contain a valid string "version".`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
process.stdout.write(`${manifest.version}\n`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function installCodexPluginConfig(configPath, repoRoot, marketplaceName, pluginName) {
|
|
288
|
+
const text = readTextIfExists(configPath);
|
|
289
|
+
const repoRootPath = path.resolve(repoRoot);
|
|
290
|
+
const nextText = applyTomlSectionUpdates(
|
|
291
|
+
text,
|
|
292
|
+
{
|
|
293
|
+
removeHeaders: [
|
|
294
|
+
'mcp_servers."open-codex-computer-use"',
|
|
295
|
+
'mcp_servers."open-computer-use"',
|
|
296
|
+
],
|
|
297
|
+
upserts: [
|
|
298
|
+
{
|
|
299
|
+
header: `marketplaces.${marketplaceName}`,
|
|
300
|
+
body: `source_type = "local"\nsource = ${JSON.stringify(repoRootPath)}`,
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
header: `plugins."${pluginName}@${marketplaceName}"`,
|
|
304
|
+
body: "enabled = true",
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
configPath,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
ensureParentDir(configPath);
|
|
312
|
+
writeFileSync(configPath, nextText, "utf8");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function copyIntoDir(targetDir, sourcePaths) {
|
|
316
|
+
if (sourcePaths.length === 0) {
|
|
317
|
+
fail("copy-into-dir requires at least one source path.");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
mkdirSync(targetDir, { recursive: true });
|
|
321
|
+
|
|
322
|
+
for (const sourcePath of sourcePaths) {
|
|
323
|
+
if (!existsSync(sourcePath)) {
|
|
324
|
+
fail(`Source path does not exist: ${sourcePath}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const destinationPath = path.join(targetDir, path.basename(sourcePath));
|
|
328
|
+
rmSync(destinationPath, { recursive: true, force: true });
|
|
329
|
+
cpSync(sourcePath, destinationPath, { recursive: true });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function main(argv) {
|
|
334
|
+
const [command, ...args] = argv;
|
|
335
|
+
switch (command) {
|
|
336
|
+
case "claude-mcp":
|
|
337
|
+
if (args.length !== 4) {
|
|
338
|
+
usage();
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
installClaudeMcp(...args);
|
|
342
|
+
return;
|
|
343
|
+
case "codex-mcp":
|
|
344
|
+
if (args.length !== 3) {
|
|
345
|
+
usage();
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
installCodexMcp(...args);
|
|
349
|
+
return;
|
|
350
|
+
case "codex-plugin-version":
|
|
351
|
+
if (args.length !== 1) {
|
|
352
|
+
usage();
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
printCodexPluginVersion(args[0]);
|
|
356
|
+
return;
|
|
357
|
+
case "codex-plugin-config":
|
|
358
|
+
if (args.length !== 4) {
|
|
359
|
+
usage();
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
installCodexPluginConfig(...args);
|
|
363
|
+
return;
|
|
364
|
+
case "copy-into-dir":
|
|
365
|
+
if (args.length < 2) {
|
|
366
|
+
usage();
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
copyIntoDir(args[0], args.slice(1));
|
|
370
|
+
return;
|
|
371
|
+
default:
|
|
372
|
+
usage();
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
main(process.argv.slice(2));
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -11,7 +11,7 @@ const mcpConfig = {
|
|
|
11
11
|
};
|
|
12
12
|
const lines = [
|
|
13
13
|
"",
|
|
14
|
-
"Installed open-computer-use@0.1.
|
|
14
|
+
"Installed open-computer-use@0.1.20.",
|
|
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
|
"",
|