minecodex 0.1.0 → 0.1.2

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
@@ -17,6 +17,10 @@ npm install -g minecodex
17
17
  mcx install
18
18
  ```
19
19
 
20
+ `mcx install` 会关闭当前正在运行的 Codex 窗口,再以 MineCodex 托管方式重新打开;
21
+ 请先保存正在编辑的内容,并让安装命令完整运行到成功提示。安装后,macOS 的
22
+ “登录项与扩展”中会显示名为 **MineCodex** 的后台项目,用于在登录后恢复插件。
23
+
20
24
  安装后三个插件默认开启。打开本地控制台:
21
25
 
22
26
  ```bash
@@ -34,7 +38,8 @@ mcx status [--json]
34
38
  mcx restart
35
39
  ```
36
40
 
37
- 关闭插件只会停止注册,不会删除数据。`mcx uninstall` 默认保留 MineCodex 设置、
41
+ 关闭插件只会停用对应功能,不会删除数据;保存开关后运行 `mcx restart` 应用变更。
42
+ `mcx uninstall` 会移除名为 MineCodex 的后台项目,并默认保留 MineCodex 设置、
38
43
  Notes 数据和 Images 索引;只有显式传入 `--purge` 才会删除 MineCodex 自有数据。
39
44
  MineCodex 永远不会删除 `~/.codex/generated_images` 中的原图或 Notes 引用的源文件。
40
45
 
@@ -87,7 +87,7 @@ HTTP + Images UI
87
87
  - Images 服务需要保持运行,才会持续监听新图片;侧边栏注册由 `CodexRuntimeHost` 统一维持。
88
88
  - 每次服务启动都会先开放 health,再在后台补扫停机期间新增的图片并刷新来源名称;初始补扫完成后,运行期间的新图片由文件监听自动加入。手动 Refresh 会再次补扫来源和 Archive 状态,但不会重置筛选、图板模式、详情、滚动位置或列数。刷新 ring 至少显示 1 秒。
89
89
  - 侧边栏使用 Codex 当前未公开的 CDP / DOM 能力,Codex UI 更新后只需调整统一宿主;独立 Web UI 和路径索引不受影响。
90
- - 当前 Codex 的 CSP 会屏蔽普通本地 iframe。统一宿主会先等待原生侧栏完成渲染,再启用 CSP bypass、注册 document-start 脚本,并替换当前 document 中旧的 MineCodex runtime 后显式完成注入;当前实现不会调用会让用户看到页面刷新的 `Page.reload`(Issue #1)。
90
+ - 当前 Codex 的 CSP 会屏蔽普通本地 iframe。统一宿主会先启用 CSP bypass 并注册 document-start 脚本;尚未完成该初始化的首次或旧版 document 会受控 reload 一次。完成初始化后会留下 document 标记,后续 RuntimeHost 重启只替换旧的 MineCodex runtime,不再刷新 Codex 页面(Issue #1)。
91
91
  - 详情打开时会从本机 Session JSONL 按需读取该 ImageGen item 的真实 `revised_prompt` 并缓存到索引;历史记录缺少该事件时明确显示 Prompt 不可用,不从附近文本猜测。
92
92
  - 当前没有发现 Codex 会因运行时间增长而自动清理 `generated_images` 的证据。如果未来观察到真实清理,再决定是否增加可选备份。
93
93
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "id": "images",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "label": {
6
6
  "en": "Images",
7
7
  "zh-CN": "图片"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "id": "model-slider",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "label": {
6
6
  "en": "Model Slider",
7
7
  "zh-CN": "模型滑条"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "id": "notes",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "label": {
6
6
  "en": "Notes",
7
7
  "zh-CN": "笔记"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,6 +16,7 @@
16
16
  "LICENSE",
17
17
  "packages/cli/bin",
18
18
  "packages/cli/src",
19
+ "packages/cli/native/MineCodex.app",
19
20
  "packages/runtime-host/src",
20
21
  "features/model-slider/assets",
21
22
  "features/model-slider/codex-feature.json",
@@ -39,10 +40,11 @@
39
40
  },
40
41
  "scripts": {
41
42
  "start": "node packages/cli/bin/mcx.mjs serve",
42
- "test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs",
43
+ "test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs apps/dashboard/test/*.test.mjs",
43
44
  "test:cli": "node --test packages/cli/test/*.test.mjs",
44
45
  "test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
45
46
  "check": "node --check packages/cli/bin/mcx.mjs && node --check packages/cli/src/*.mjs && node --check packages/runtime-host/src/*.mjs",
47
+ "build:service": "scripts/build-service-app.zsh",
46
48
  "pack:check": "npm pack --dry-run",
47
49
  "pack:audit": "node scripts/audit-package.mjs",
48
50
  "prepublishOnly": "npm test && npm run check && npm run pack:audit"
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>en</string>
7
+ <key>CFBundleDisplayName</key>
8
+ <string>MineCodex</string>
9
+ <key>CFBundleExecutable</key>
10
+ <string>MineCodexServiceManager</string>
11
+ <key>CFBundleIdentifier</key>
12
+ <string>com.vontean.minecodex.background-service-manager</string>
13
+ <key>CFBundleInfoDictionaryVersion</key>
14
+ <string>6.0</string>
15
+ <key>CFBundleName</key>
16
+ <string>MineCodex</string>
17
+ <key>CFBundlePackageType</key>
18
+ <string>APPL</string>
19
+ <key>CFBundleShortVersionString</key>
20
+ <string>1.0.0</string>
21
+ <key>CFBundleVersion</key>
22
+ <string>1</string>
23
+ <key>LSBackgroundOnly</key>
24
+ <true/>
25
+ <key>LSMinimumSystemVersion</key>
26
+ <string>13.0</string>
27
+ </dict>
28
+ </plist>
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.vontean.minecodex.background</string>
7
+ <key>BundleProgram</key>
8
+ <string>Contents/Resources/MineCodexServiceHost</string>
9
+ <key>RunAtLoad</key>
10
+ <true/>
11
+ <key>KeepAlive</key>
12
+ <dict>
13
+ <key>SuccessfulExit</key>
14
+ <false/>
15
+ </dict>
16
+ <key>ProcessType</key>
17
+ <string>Interactive</string>
18
+ </dict>
19
+ </plist>
@@ -0,0 +1,135 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>files</key>
6
+ <dict>
7
+ <key>Resources/MineCodexServiceHost</key>
8
+ <data>
9
+ kB0iYBcULzqcS1yOgYpY3LZjeq4=
10
+ </data>
11
+ </dict>
12
+ <key>files2</key>
13
+ <dict>
14
+ <key>Library/LaunchAgents/com.vontean.minecodex.background.plist</key>
15
+ <dict>
16
+ <key>hash2</key>
17
+ <data>
18
+ mGWkf+X17Pau3uy8xUUB1s+JXofeKYKNRzFPk3FXeaA=
19
+ </data>
20
+ </dict>
21
+ <key>Resources/MineCodexServiceHost</key>
22
+ <dict>
23
+ <key>hash2</key>
24
+ <data>
25
+ b9bnzjgASA30QyXiczJMxHbOYKm7WG+j24ZfZ1povzc=
26
+ </data>
27
+ </dict>
28
+ </dict>
29
+ <key>rules</key>
30
+ <dict>
31
+ <key>^Resources/</key>
32
+ <true/>
33
+ <key>^Resources/.*\.lproj/</key>
34
+ <dict>
35
+ <key>optional</key>
36
+ <true/>
37
+ <key>weight</key>
38
+ <real>1000</real>
39
+ </dict>
40
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
41
+ <dict>
42
+ <key>omit</key>
43
+ <true/>
44
+ <key>weight</key>
45
+ <real>1100</real>
46
+ </dict>
47
+ <key>^Resources/Base\.lproj/</key>
48
+ <dict>
49
+ <key>weight</key>
50
+ <real>1010</real>
51
+ </dict>
52
+ <key>^version.plist$</key>
53
+ <true/>
54
+ </dict>
55
+ <key>rules2</key>
56
+ <dict>
57
+ <key>.*\.dSYM($|/)</key>
58
+ <dict>
59
+ <key>weight</key>
60
+ <real>11</real>
61
+ </dict>
62
+ <key>^(.*/)?\.DS_Store$</key>
63
+ <dict>
64
+ <key>omit</key>
65
+ <true/>
66
+ <key>weight</key>
67
+ <real>2000</real>
68
+ </dict>
69
+ <key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
70
+ <dict>
71
+ <key>nested</key>
72
+ <true/>
73
+ <key>weight</key>
74
+ <real>10</real>
75
+ </dict>
76
+ <key>^.*</key>
77
+ <true/>
78
+ <key>^Info\.plist$</key>
79
+ <dict>
80
+ <key>omit</key>
81
+ <true/>
82
+ <key>weight</key>
83
+ <real>20</real>
84
+ </dict>
85
+ <key>^PkgInfo$</key>
86
+ <dict>
87
+ <key>omit</key>
88
+ <true/>
89
+ <key>weight</key>
90
+ <real>20</real>
91
+ </dict>
92
+ <key>^Resources/</key>
93
+ <dict>
94
+ <key>weight</key>
95
+ <real>20</real>
96
+ </dict>
97
+ <key>^Resources/.*\.lproj/</key>
98
+ <dict>
99
+ <key>optional</key>
100
+ <true/>
101
+ <key>weight</key>
102
+ <real>1000</real>
103
+ </dict>
104
+ <key>^Resources/.*\.lproj/locversion.plist$</key>
105
+ <dict>
106
+ <key>omit</key>
107
+ <true/>
108
+ <key>weight</key>
109
+ <real>1100</real>
110
+ </dict>
111
+ <key>^Resources/Base\.lproj/</key>
112
+ <dict>
113
+ <key>weight</key>
114
+ <real>1010</real>
115
+ </dict>
116
+ <key>^[^/]+$</key>
117
+ <dict>
118
+ <key>nested</key>
119
+ <true/>
120
+ <key>weight</key>
121
+ <real>10</real>
122
+ </dict>
123
+ <key>^embedded\.provisionprofile$</key>
124
+ <dict>
125
+ <key>weight</key>
126
+ <real>20</real>
127
+ </dict>
128
+ <key>^version\.plist$</key>
129
+ <dict>
130
+ <key>weight</key>
131
+ <real>20</real>
132
+ </dict>
133
+ </dict>
134
+ </dict>
135
+ </plist>
@@ -0,0 +1,18 @@
1
+ # Stable MineCodex background host
2
+
3
+ `MineCodex.app` is the small, universal native owner of MineCodex's macOS
4
+ background item. The npm package ships the built app only; native source and the
5
+ build script remain development inputs.
6
+
7
+ The production app bundle is intentionally versioned independently from the npm
8
+ package. Its ad-hoc code identity must stay byte-for-byte stable across ordinary
9
+ JavaScript releases because macOS records the helper's designated requirement.
10
+ Do not rebuild or change the bundle for an npm version bump. If native behavior
11
+ must change, plan an explicit signed-helper migration or introduce a new service
12
+ identifier and verify the macOS Background Items upgrade path before release.
13
+
14
+ Rebuild deliberately with:
15
+
16
+ ```bash
17
+ npm run build:service
18
+ ```
@@ -23,6 +23,12 @@ async function exists(filePath) {
23
23
  }
24
24
  }
25
25
 
26
+ async function serviceInstalled(platform, paths) {
27
+ if (platform.isServiceInstalled) return platform.isServiceInstalled(paths);
28
+ return (paths.serviceManagerPath ? exists(paths.serviceManagerPath) : false)
29
+ || (paths.launchAgentPath ? exists(paths.launchAgentPath) : false);
30
+ }
31
+
26
32
  async function waitForService(port, { fetchImpl = fetch, timeoutMs = 20_000 } = {}) {
27
33
  const deadline = Date.now() + timeoutMs;
28
34
  while (Date.now() < deadline) {
@@ -151,7 +157,7 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
151
157
  async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
152
158
  const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
153
159
  if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl, timeoutMs: 750 }))) {
154
- if (!(await exists(paths.launchAgentPath))) throw new Error("MineCodex is not installed. Run `mcx install` first.");
160
+ if (!(await serviceInstalled(platform, paths))) throw new Error("MineCodex is not installed. Run `mcx install` first.");
155
161
  await platform.startService(paths);
156
162
  if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) throw new Error("MineCodex service could not be started.");
157
163
  }
@@ -181,16 +187,16 @@ function featureStatusText(feature) {
181
187
  return `${copy[state] ?? state}${failure}`;
182
188
  }
183
189
 
184
- async function statusCommand({ paths, jsonOutput, output, fetchImpl }) {
190
+ async function statusCommand({ paths, platform, jsonOutput, output, fetchImpl }) {
185
191
  let status;
186
192
  try {
187
193
  const response = await authorizedFetch(paths, "/api/status", {}, { fetchImpl });
188
194
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
189
- status = { installed: await exists(paths.launchAgentPath), service: { running: true }, ...await response.json() };
195
+ status = { installed: await serviceInstalled(platform, paths), service: { running: true }, ...await response.json() };
190
196
  } catch {
191
197
  const config = await readConfig(paths.configPath).catch(() => null);
192
198
  status = {
193
- installed: await exists(paths.launchAgentPath),
199
+ installed: await serviceInstalled(platform, paths),
194
200
  version: packageMetadata.version,
195
201
  service: { running: false },
196
202
  runtime: { running: false },
@@ -267,7 +273,7 @@ async function updateCommand({ paths, platform, cliPath, output, npm = createNpm
267
273
  output(`MineCodex ${packageMetadata.version} is already up to date.`);
268
274
  return;
269
275
  }
270
- const wasInstalled = await exists(paths.launchAgentPath);
276
+ const wasInstalled = await serviceInstalled(platform, paths);
271
277
  const previousService = wasInstalled
272
278
  ? (await serviceSnapshot(platform, paths) ?? { running: true, plist: { exists: true } })
273
279
  : null;
@@ -276,7 +282,7 @@ async function updateCommand({ paths, platform, cliPath, output, npm = createNpm
276
282
  if (!rollbackPath) throw new Error("Could not create a rollback archive for the installed MineCodex version.");
277
283
  try {
278
284
  if (wasInstalled && previousService?.running) {
279
- await platform.stopService({ ignoreErrors: false });
285
+ await platform.stopService(paths, { ignoreErrors: false });
280
286
  await platform.assertServiceStopped?.();
281
287
  }
282
288
  await npm.installGlobal("minecodex@latest");
@@ -378,7 +384,7 @@ export async function runCli(argv, {
378
384
  if (["--version", "-v"].includes(command)) return output(packageMetadata.version);
379
385
  if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, nodeVersion });
380
386
  if (command === "open" || command === "gui") return openCommand({ paths, platform, serviceProbe, fetchImpl });
381
- if (command === "status") return statusCommand({ paths, jsonOutput: args.includes("--json"), output, fetchImpl });
387
+ if (command === "status") return statusCommand({ paths, platform, jsonOutput: args.includes("--json"), output, fetchImpl });
382
388
  if (command === "restart") return restartCommand({ paths, output, fetchImpl });
383
389
  if (command === "uninstall" || command === "remove") {
384
390
  return uninstallCommand({ paths, platform, purge: args.includes("--purge"), output, cliPath });
@@ -10,6 +10,8 @@ export const PACKAGE_ROOT = path.resolve(
10
10
  export function resolvePaths({ homeDir = os.homedir() } = {}) {
11
11
  const supportDir = path.join(homeDir, "Library", "Application Support", "MineCodex");
12
12
  const logsDir = path.join(homeDir, "Library", "Logs", "MineCodex");
13
+ const serviceDir = path.join(supportDir, "service");
14
+ const serviceAppPath = path.join(serviceDir, "MineCodex.app");
13
15
  return Object.freeze({
14
16
  homeDir,
15
17
  packageRoot: PACKAGE_ROOT,
@@ -18,12 +20,18 @@ export function resolvePaths({ homeDir = os.homedir() } = {}) {
18
20
  runtimeEntry: path.join(PACKAGE_ROOT, "packages", "runtime-host", "src", "main.mjs"),
19
21
  supportDir,
20
22
  logsDir,
23
+ serviceDir,
24
+ serviceBundleSource: path.join(PACKAGE_ROOT, "packages", "cli", "native", "MineCodex.app"),
25
+ serviceAppPath,
26
+ serviceManagerPath: path.join(serviceAppPath, "Contents", "MacOS", "MineCodexServiceManager"),
27
+ serviceRuntimeConfigPath: path.join(supportDir, "service-runtime.plist"),
21
28
  configPath: path.join(supportDir, "config.json"),
22
29
  tokenPath: path.join(supportDir, "control-token"),
23
30
  pidPath: path.join(supportDir, "service.pid"),
24
31
  codexPidPath: path.join(supportDir, "codex.pid"),
25
32
  runtimeReadyPath: path.join(supportDir, "runtime-ready.json"),
26
33
  profileDir: path.join(supportDir, "codex-profile"),
34
+ // v0.1.0 used a directly installed plist. Keep the path only for migration and cleanup.
27
35
  launchAgentPath: path.join(homeDir, "Library", "LaunchAgents", "com.vontean.minecodex.plist"),
28
36
  serviceLogPath: path.join(logsDir, "runtime.log"),
29
37
  serviceErrorLogPath: path.join(logsDir, "runtime-error.log"),
@@ -1,16 +1,18 @@
1
1
  import { execFile as execFileCallback } from "node:child_process";
2
- import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { promisify } from "node:util";
6
6
 
7
7
  const defaultExecFile = promisify(execFileCallback);
8
- const defaultFileSystem = Object.freeze({ mkdir, readFile, stat, unlink, writeFile });
8
+ const defaultFileSystem = Object.freeze({ mkdir, readFile, rename, rm, stat, unlink, writeFile });
9
9
 
10
- export const SERVICE_LABEL = "com.vontean.minecodex";
10
+ export const SERVICE_LABEL = "com.vontean.minecodex.background";
11
+ export const LEGACY_SERVICE_LABEL = "com.vontean.minecodex";
11
12
  export const CODEX_APP_PATH = "/Applications/ChatGPT.app";
12
13
  export const CODEX_EXECUTABLE = `${CODEX_APP_PATH}/Contents/MacOS/ChatGPT`;
13
14
  export const MIN_NODE_VERSION = Object.freeze({ major: 22, minor: 5, patch: 0 });
15
+ export const MIN_MACOS_VERSION = Object.freeze({ major: 13, darwinMajor: 22 });
14
16
 
15
17
  function xml(value) {
16
18
  return String(value)
@@ -21,38 +23,21 @@ function xml(value) {
21
23
  .replaceAll("'", "&apos;");
22
24
  }
23
25
 
24
- export function buildLaunchAgent({ nodePath, cliPath, paths }) {
26
+ export function buildServiceRuntimeConfig({ nodePath, cliPath, paths }) {
25
27
  return `<?xml version="1.0" encoding="UTF-8"?>
26
28
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
27
29
  <plist version="1.0">
28
30
  <dict>
29
- <key>Label</key>
30
- <string>${SERVICE_LABEL}</string>
31
- <key>ProgramArguments</key>
32
- <array>
33
- <string>${xml(nodePath)}</string>
34
- <string>${xml(cliPath)}</string>
35
- <string>serve</string>
36
- <string>--service</string>
37
- </array>
38
- <key>RunAtLoad</key>
39
- <true/>
40
- <key>KeepAlive</key>
41
- <dict>
42
- <key>SuccessfulExit</key>
43
- <false/>
44
- </dict>
45
- <key>ProcessType</key>
46
- <string>Interactive</string>
47
- <key>StandardOutPath</key>
31
+ <key>NodePath</key>
32
+ <string>${xml(nodePath)}</string>
33
+ <key>CLIPath</key>
34
+ <string>${xml(cliPath)}</string>
35
+ <key>StandardOutputPath</key>
48
36
  <string>${xml(paths.serviceLogPath)}</string>
49
37
  <key>StandardErrorPath</key>
50
38
  <string>${xml(paths.serviceErrorLogPath)}</string>
51
- <key>EnvironmentVariables</key>
52
- <dict>
53
- <key>PATH</key>
54
- <string>${xml(path.dirname(nodePath))}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
55
- </dict>
39
+ <key>Path</key>
40
+ <string>${xml(path.dirname(nodePath))}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
56
41
  </dict>
57
42
  </plist>
58
43
  `;
@@ -155,10 +140,31 @@ function serviceTarget(uid) {
155
140
  return `gui/${uid}/${SERVICE_LABEL}`;
156
141
  }
157
142
 
143
+ function legacyServiceTarget(uid) {
144
+ return `gui/${uid}/${LEGACY_SERVICE_LABEL}`;
145
+ }
146
+
147
+ function appServiceStatus(output) {
148
+ const match = String(output).match(/(?:^|\n)status=([a-z-]+)(?:\n|$)/);
149
+ return match?.[1] ?? "unknown";
150
+ }
151
+
152
+ function assertOwnedServicePaths(paths) {
153
+ const root = path.parse(paths.serviceDir ?? "").root;
154
+ if (!path.isAbsolute(paths.serviceDir)
155
+ || paths.serviceDir === root
156
+ || path.dirname(paths.serviceDir) === root
157
+ || path.basename(paths.serviceDir) !== "service"
158
+ || paths.serviceAppPath !== path.join(paths.serviceDir, "MineCodex.app")) {
159
+ throw new Error(`Refusing unsafe MineCodex service path: ${paths.serviceAppPath}`);
160
+ }
161
+ }
162
+
158
163
  export class MacPlatformAdapter {
159
164
  constructor({
160
165
  uid = process.getuid?.() ?? os.userInfo().uid,
161
166
  platform = process.platform,
167
+ osRelease = os.release(),
162
168
  homeDir = os.homedir(),
163
169
  execFile = defaultExecFile,
164
170
  fileSystem = defaultFileSystem,
@@ -168,6 +174,7 @@ export class MacPlatformAdapter {
168
174
  } = {}) {
169
175
  this.uid = uid;
170
176
  this.platform = platform;
177
+ this.osRelease = osRelease;
171
178
  this.homeDir = homeDir;
172
179
  this.execFile = execFile;
173
180
  this.fileSystem = fileSystem;
@@ -180,6 +187,10 @@ export class MacPlatformAdapter {
180
187
  if (this.platform !== "darwin") {
181
188
  throw new Error("MineCodex v1 installation currently supports macOS only.");
182
189
  }
190
+ const darwinMajor = Number.parseInt(this.osRelease.split(".")[0], 10);
191
+ if (!Number.isInteger(darwinMajor) || darwinMajor < MIN_MACOS_VERSION.darwinMajor) {
192
+ throw new Error(`MineCodex requires macOS ${MIN_MACOS_VERSION.major} or newer.`);
193
+ }
183
194
  }
184
195
 
185
196
  async assertCodexInstalled() {
@@ -202,9 +213,9 @@ export class MacPlatformAdapter {
202
213
  return this.codexInstallation ?? this.assertCodexInstalled();
203
214
  }
204
215
 
205
- async isServiceRunning() {
216
+ async isLaunchdServiceRunning(target) {
206
217
  try {
207
- await this.execFile("/bin/launchctl", ["print", serviceTarget(this.uid)]);
218
+ await this.execFile("/bin/launchctl", ["print", target]);
208
219
  return true;
209
220
  } catch (error) {
210
221
  if (serviceIsAbsent(error)) return false;
@@ -212,6 +223,41 @@ export class MacPlatformAdapter {
212
223
  }
213
224
  }
214
225
 
226
+ async isServiceRunning() {
227
+ return this.isLaunchdServiceRunning(serviceTarget(this.uid));
228
+ }
229
+
230
+ async pathExists(filePath) {
231
+ try {
232
+ if (this.fileSystem.stat) await this.fileSystem.stat(filePath);
233
+ else await this.fileSystem.readFile(filePath);
234
+ return true;
235
+ } catch (error) {
236
+ if (error.code === "ENOENT") return false;
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ async serviceManagerStatus(paths) {
242
+ if (!(await this.pathExists(paths.serviceManagerPath))) return "not-found";
243
+ const { stdout } = await this.execFile(paths.serviceManagerPath, ["status"]);
244
+ return appServiceStatus(stdout);
245
+ }
246
+
247
+ async isServiceInstalled(paths) {
248
+ return (await this.pathExists(paths.serviceManagerPath))
249
+ || (await this.pathExists(paths.launchAgentPath));
250
+ }
251
+
252
+ async writeServiceRuntimeConfig(paths, content, mode = 0o600) {
253
+ const temporaryPath = `${paths.serviceRuntimeConfigPath}.installing`;
254
+ await this.fileSystem.unlink(temporaryPath).catch((error) => {
255
+ if (error.code !== "ENOENT") throw error;
256
+ });
257
+ await this.fileSystem.writeFile(temporaryPath, content, { mode });
258
+ await this.fileSystem.rename(temporaryPath, paths.serviceRuntimeConfigPath);
259
+ }
260
+
215
261
  async snapshotServiceState(paths) {
216
262
  let plist;
217
263
  try {
@@ -227,45 +273,123 @@ export class MacPlatformAdapter {
227
273
  if (error.code !== "ENOENT") throw error;
228
274
  plist = { exists: false, content: null, mode: null };
229
275
  }
230
- return { plist, running: await this.isServiceRunning() };
276
+ const appExists = await this.pathExists(paths.serviceManagerPath);
277
+ let runtimeConfig = { exists: false, content: null, mode: null };
278
+ if (appExists && paths.serviceRuntimeConfigPath) {
279
+ try {
280
+ const metadataPromise = this.fileSystem.stat
281
+ ? this.fileSystem.stat(paths.serviceRuntimeConfigPath).catch(() => null)
282
+ : Promise.resolve(null);
283
+ const [content, metadata] = await Promise.all([
284
+ this.fileSystem.readFile(paths.serviceRuntimeConfigPath, "utf8"),
285
+ metadataPromise,
286
+ ]);
287
+ runtimeConfig = { exists: true, content, mode: metadata?.mode };
288
+ } catch (error) {
289
+ if (error.code !== "ENOENT") throw error;
290
+ }
291
+ }
292
+ const running = await this.isServiceRunning()
293
+ || (plist.exists && await this.isLaunchdServiceRunning(legacyServiceTarget(this.uid)));
294
+ return {
295
+ app: {
296
+ exists: appExists,
297
+ status: appExists ? await this.serviceManagerStatus(paths) : "not-found",
298
+ runtimeConfig,
299
+ },
300
+ plist,
301
+ running,
302
+ };
231
303
  }
232
304
 
233
305
  async installService({ cliPath, paths, start = true }) {
234
- await this.fileSystem.mkdir(path.dirname(paths.launchAgentPath), { recursive: true });
306
+ const stagingAppPath = path.join(paths.serviceDir, ".MineCodex.app.installing");
307
+ assertOwnedServicePaths(paths);
308
+ await this.stopService(paths, { ignoreErrors: false });
309
+ await this.fileSystem.mkdir(paths.serviceDir, { recursive: true, mode: 0o700 });
235
310
  await this.fileSystem.mkdir(paths.logsDir, { recursive: true, mode: 0o700 });
236
- const plist = buildLaunchAgent({ nodePath: process.execPath, cliPath, paths });
237
- await this.fileSystem.writeFile(paths.launchAgentPath, plist, { mode: 0o600 });
238
- await this.stopService({ ignoreErrors: true });
239
- await this.execFile("/bin/launchctl", ["bootstrap", `gui/${this.uid}`, paths.launchAgentPath]);
240
- if (start) {
241
- await this.execFile("/bin/launchctl", ["kickstart", "-k", serviceTarget(this.uid)]);
242
- } else {
243
- await this.stopService({ ignoreErrors: true });
311
+ await this.writeServiceRuntimeConfig(
312
+ paths,
313
+ buildServiceRuntimeConfig({ nodePath: process.execPath, cliPath, paths }),
314
+ );
315
+ await this.fileSystem.rm(stagingAppPath, { recursive: true, force: true });
316
+ try {
317
+ await this.execFile("/usr/bin/ditto", [paths.serviceBundleSource, stagingAppPath]);
318
+ await this.execFile("/usr/bin/codesign", ["--verify", "--deep", "--strict", stagingAppPath]);
319
+ await this.fileSystem.rm(paths.serviceAppPath, { recursive: true, force: true });
320
+ await this.fileSystem.rename(stagingAppPath, paths.serviceAppPath);
321
+ } catch (error) {
322
+ await this.fileSystem.rm(stagingAppPath, { recursive: true, force: true }).catch(() => {});
323
+ throw error;
244
324
  }
325
+ await this.fileSystem.unlink(paths.launchAgentPath).catch((error) => {
326
+ if (error.code !== "ENOENT") throw error;
327
+ });
328
+ if (start) await this.startService(paths);
245
329
  }
246
330
 
247
- async stopService({ ignoreErrors = false } = {}) {
331
+ async stopService(paths, { ignoreErrors = false } = {}) {
332
+ let managerError = null;
333
+ if (paths?.serviceManagerPath && await this.pathExists(paths.serviceManagerPath)) {
334
+ try {
335
+ await this.execFile(paths.serviceManagerPath, ["unregister"]);
336
+ } catch (error) {
337
+ managerError = error;
338
+ }
339
+ }
248
340
  try {
249
341
  await this.execFile("/bin/launchctl", ["bootout", serviceTarget(this.uid)]);
250
342
  } catch (error) {
251
- if (!ignoreErrors || !serviceIsAbsent(error)) throw error;
343
+ if (!serviceIsAbsent(error)) throw error;
252
344
  }
345
+ try {
346
+ await this.execFile("/bin/launchctl", ["bootout", legacyServiceTarget(this.uid)]);
347
+ } catch (error) {
348
+ if (!serviceIsAbsent(error)) throw error;
349
+ }
350
+ if (managerError && !ignoreErrors) throw managerError;
253
351
  }
254
352
 
255
353
  async startService(paths) {
256
- if (!(await this.isServiceRunning())) {
257
- await this.execFile("/bin/launchctl", ["bootstrap", `gui/${this.uid}`, paths.launchAgentPath]);
354
+ const status = await this.serviceManagerStatus(paths);
355
+ if (status === "requires-approval") {
356
+ throw new Error(
357
+ "MineCodex background service is disabled. Enable MineCodex in System Settings > General > Login Items & Extensions, then run `mcx install` again.",
358
+ );
359
+ }
360
+ if (status !== "enabled") {
361
+ const { stdout } = await this.execFile(paths.serviceManagerPath, ["register"]);
362
+ const registeredStatus = appServiceStatus(stdout);
363
+ if (registeredStatus !== "enabled") {
364
+ throw new Error(`MineCodex background service registration requires attention: ${registeredStatus}`);
365
+ }
258
366
  }
259
367
  await this.execFile("/bin/launchctl", ["kickstart", "-k", serviceTarget(this.uid)]);
260
368
  }
261
369
 
262
370
  async assertServiceStopped() {
263
- if (await this.isServiceRunning()) throw new Error("MineCodex launchd service is still running.");
371
+ const [current, legacy] = await Promise.all([
372
+ this.isServiceRunning(),
373
+ this.isLaunchdServiceRunning(legacyServiceTarget(this.uid)),
374
+ ]);
375
+ if (current || legacy) throw new Error("MineCodex launchd service is still running.");
264
376
  }
265
377
 
266
378
  async restoreServiceState(snapshot, { cliPath, paths } = {}) {
267
- await this.stopService({ ignoreErrors: true });
379
+ await this.removeService(paths);
268
380
  await this.assertServiceStopped();
381
+ if (snapshot?.app?.exists) {
382
+ await this.installService({ cliPath, paths, start: false });
383
+ if (snapshot.app.runtimeConfig?.exists) {
384
+ await this.writeServiceRuntimeConfig(
385
+ paths,
386
+ snapshot.app.runtimeConfig.content,
387
+ snapshot.app.runtimeConfig.mode ? snapshot.app.runtimeConfig.mode & 0o777 : 0o600,
388
+ );
389
+ }
390
+ if (snapshot.app.status === "enabled") await this.startService(paths);
391
+ return snapshot;
392
+ }
269
393
  if (snapshot?.plist?.exists) {
270
394
  await this.fileSystem.mkdir(path.dirname(paths.launchAgentPath), { recursive: true });
271
395
  await this.fileSystem.writeFile(paths.launchAgentPath, snapshot.plist.content, {
@@ -276,20 +400,29 @@ export class MacPlatformAdapter {
276
400
  if (error.code !== "ENOENT") throw error;
277
401
  });
278
402
  }
279
- if (snapshot?.running) {
280
- if (!snapshot?.plist?.exists) throw new Error("Cannot restore a running service without its plist.");
403
+ if (snapshot?.running && snapshot?.plist?.exists) {
281
404
  await this.execFile("/bin/launchctl", ["bootstrap", `gui/${this.uid}`, paths.launchAgentPath]);
282
- await this.execFile("/bin/launchctl", ["kickstart", "-k", serviceTarget(this.uid)]);
405
+ await this.execFile("/bin/launchctl", ["kickstart", "-k", legacyServiceTarget(this.uid)]);
283
406
  }
284
407
  return snapshot;
285
408
  }
286
409
 
287
410
  async removeService(paths) {
288
- await this.stopService({ ignoreErrors: true });
411
+ assertOwnedServicePaths(paths);
412
+ await this.stopService(paths, { ignoreErrors: false });
289
413
  await this.assertServiceStopped();
290
414
  await this.fileSystem.unlink(paths.launchAgentPath).catch((error) => {
291
415
  if (error.code !== "ENOENT") throw error;
292
416
  });
417
+ if (paths.serviceRuntimeConfigPath) {
418
+ await this.fileSystem.unlink(paths.serviceRuntimeConfigPath).catch((error) => {
419
+ if (error.code !== "ENOENT") throw error;
420
+ });
421
+ await this.fileSystem.unlink(`${paths.serviceRuntimeConfigPath}.installing`).catch((error) => {
422
+ if (error.code !== "ENOENT") throw error;
423
+ });
424
+ }
425
+ await this.fileSystem.rm(paths.serviceDir, { recursive: true, force: true });
293
426
  }
294
427
 
295
428
  async listCodexProcesses() {
@@ -141,6 +141,9 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
141
141
 
142
142
  - 只在 top frame 安装 Runtime,绝不向业务 iframe 注入 binding token。
143
143
  - Watch / refresh 串行;已连接 Renderer 不重复连接,关闭 target 会清理状态。
144
+ - 本地 Surface 注入前先启用 CSP bypass 并注册 document-start 脚本;缺少初始化标记的
145
+ 首次或旧版 document 只受控 reload 一次。标记存在时,新的 RuntimeHost 会话直接
146
+ 替换旧 Runtime,不刷新当前 Codex 页面。
144
147
  - 入口由 Renderer 内的 MutationObserver 幂等挂载,React 重绘不会产生重复入口。
145
148
  - Summary 根据对话主区域宽度连续派生 `overlay / shift / gutter`:小于 1096px
146
149
  使用临时 Popover;1096–1535px 预留 316px 并把对话内容左移 158px;更宽时
@@ -5,6 +5,8 @@ import path from "node:path";
5
5
 
6
6
  const RUNTIME_VERSION = 99;
7
7
  const HOST_BINDING_NAME = "__codexPersonalHostAction";
8
+ const CSP_BOOTSTRAP_VERSION = 1;
9
+ const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
8
10
 
9
11
  export const RESPONSIVE_SUMMARY_LAYOUT = Object.freeze({
10
12
  contentBaseWidth: 736,
@@ -3564,6 +3566,14 @@ export function createRuntimeReadyContract({
3564
3566
  };
3565
3567
  }
3566
3568
 
3569
+ function createDocumentBootstrapSource(source) {
3570
+ return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] = ${CSP_BOOTSTRAP_VERSION};\n${source}`;
3571
+ }
3572
+
3573
+ function documentBootstrapExpression() {
3574
+ return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] === ${CSP_BOOTSTRAP_VERSION}`;
3575
+ }
3576
+
3567
3577
  async function connect(url) {
3568
3578
  const socket = new WebSocket(url);
3569
3579
  const pending = new Map();
@@ -4250,7 +4260,20 @@ export class CodexRuntime {
4250
4260
  bindingToken,
4251
4261
  runtimeSessionId: this.runtimeSessionId,
4252
4262
  });
4253
- const script = await client.send("Page.addScriptToEvaluateOnNewDocument", { source });
4263
+ const bootstrapExpression = documentBootstrapExpression();
4264
+ const script = await client.send("Page.addScriptToEvaluateOnNewDocument", {
4265
+ source: createDocumentBootstrapSource(source),
4266
+ });
4267
+ const documentWasBootstrapped = Boolean(evaluationValue(await client.send("Runtime.evaluate", {
4268
+ expression: bootstrapExpression,
4269
+ returnByValue: true,
4270
+ })));
4271
+ if (!documentWasBootstrapped) {
4272
+ const pageLoaded = client.waitFor("Page.loadEventFired", 20_000);
4273
+ await client.send("Page.reload");
4274
+ await pageLoaded;
4275
+ await waitForExpression(client, bootstrapExpression);
4276
+ }
4254
4277
  await waitForExpression(
4255
4278
  client,
4256
4279
  `document.readyState === "interactive" || document.readyState === "complete"`,