minecodex 0.1.2 → 0.1.4

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,9 +17,11 @@ npm install -g minecodex
17
17
  mcx install
18
18
  ```
19
19
 
20
- `mcx install` 会关闭当前正在运行的 Codex 窗口,再以 MineCodex 托管方式重新打开;
21
- 请先保存正在编辑的内容,并让安装命令完整运行到成功提示。安装后,macOS
22
- “登录项与扩展”中会显示名为 **MineCodex** 的后台项目,用于在登录后恢复插件。
20
+ `mcx install` 不会关闭或启动 Codex,也不会改变当前窗口。安装完成后,终端会提示
21
+ MineCodex 将在下次重启 Codex 时启用,并询问是否现在重启;默认答案是 **No**。
22
+ 只有用户明确确认,或之后主动运行 `mcx restart`,MineCodex 才会关闭并重新打开
23
+ Codex。安装后,macOS 的“登录项与扩展”中会显示名为 **MineCodex** 的后台项目;
24
+ 这个后台项目负责插件与本地控制台,不会在用户退出 Codex 后自动将它重新打开。
23
25
 
24
26
  安装后三个插件默认开启。打开本地控制台:
25
27
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "id": "images",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
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.2",
4
+ "version": "0.1.4",
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.2",
4
+ "version": "0.1.4",
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.2",
3
+ "version": "0.1.4",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,6 @@
1
1
  import { access, readFile, rm } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { createInterface } from "node:readline/promises";
3
4
  import { assertNodeSupported, MacPlatformAdapter } from "./platform.mjs";
4
5
  import { ensureConfig, ensureControlToken, readConfig } from "./config.mjs";
5
6
  import { createControlServer } from "./control-server.mjs";
@@ -98,37 +99,32 @@ async function restoreService(platform, snapshot, { cliPath, paths }) {
98
99
  return platform.restoreServiceState(snapshot, { cliPath, paths });
99
100
  }
100
101
 
101
- async function nativeSnapshot(platform) {
102
- if (platform.snapshotNativeCodexState) return platform.snapshotNativeCodexState();
103
- return { count: 0 };
104
- }
105
-
106
- async function restoreNative(platform, snapshot) {
107
- if (!snapshot?.count) return;
108
- if (platform.restoreNativeCodexState) {
109
- await platform.restoreNativeCodexState(snapshot);
110
- return;
111
- }
112
- await platform.openNativeCodex?.();
113
- }
114
-
115
102
  function appendRollbackError(error, rollbackError) {
116
103
  if (!rollbackError) return error;
117
104
  return new Error(`${error.message} Automatic rollback also failed: ${rollbackError.message}`, { cause: error });
118
105
  }
119
106
 
120
- async function installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, nodeVersion = process.versions.node }) {
107
+ export async function confirmRestartNow({ input = process.stdin, terminalOutput = process.stdout } = {}) {
108
+ if (!input.isTTY || !terminalOutput.isTTY) return false;
109
+ const prompt = createInterface({ input, output: terminalOutput });
110
+ try {
111
+ const answer = await prompt.question("Restart Codex now? [y/N] ");
112
+ return /^(y|yes)$/i.test(answer.trim());
113
+ } catch {
114
+ return false;
115
+ } finally {
116
+ prompt.close();
117
+ }
118
+ }
119
+
120
+ async function installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion = process.versions.node }) {
121
121
  assertNodeSupported(nodeVersion);
122
122
  platform.assertInstallSupported();
123
123
  await platform.assertCodexInstalled();
124
124
  const previousService = await serviceSnapshot(platform, paths);
125
- const previousNative = await nativeSnapshot(platform);
126
125
  await ensureConfig(paths.configPath);
127
126
  await ensureControlToken(paths.tokenPath);
128
- let nativeCount = 0;
129
127
  try {
130
- nativeCount = await platform.terminateNativeCodex();
131
- if (nativeCount) output(`Restarting ${nativeCount} running Codex instance${nativeCount === 1 ? "" : "s"} with MineCodex enabled…`);
132
128
  await platform.installService({ cliPath, paths, start: true });
133
129
  const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
134
130
  const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
@@ -143,15 +139,14 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
143
139
  } catch (failure) {
144
140
  rollbackError = failure;
145
141
  }
146
- try {
147
- await restoreNative(platform, previousNative?.count ? previousNative : { count: nativeCount });
148
- } catch (failure) {
149
- rollbackError = rollbackError ?? failure;
150
- }
151
142
  throw appendRollbackError(error, rollbackError);
152
143
  }
153
- output("MineCodex installed. Model Slider, Notes, and Images are enabled by default.");
154
- output("Run `mcx open` to manage plugins.");
144
+ output("MineCodex installed. It will be enabled the next time Codex restarts.");
145
+ if (await confirmRestart()) {
146
+ await restartCommand({ paths, output, fetchImpl });
147
+ } else {
148
+ output("Run `mcx restart` when you are ready.");
149
+ }
155
150
  }
156
151
 
157
152
  async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
@@ -377,12 +372,13 @@ export async function runCli(argv, {
377
372
  npm = createNpmAdapter(),
378
373
  serviceProbe,
379
374
  fetchImpl = fetch,
375
+ confirmRestart = confirmRestartNow,
380
376
  nodeVersion = process.versions.node,
381
377
  } = {}) {
382
378
  const [command = "--help", ...args] = argv;
383
379
  if (["--help", "-h", "help"].includes(command)) return printHelp(output);
384
380
  if (["--version", "-v"].includes(command)) return output(packageMetadata.version);
385
- if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, nodeVersion });
381
+ if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion });
386
382
  if (command === "open" || command === "gui") return openCommand({ paths, platform, serviceProbe, fetchImpl });
387
383
  if (command === "status") return statusCommand({ paths, platform, jsonOutput: args.includes("--json"), output, fetchImpl });
388
384
  if (command === "restart") return restartCommand({ paths, output, fetchImpl });
@@ -142,7 +142,7 @@ export class RuntimeManager {
142
142
  return next;
143
143
  }
144
144
 
145
- async startInternal() {
145
+ async startInternal({ launchCodex = false } = {}) {
146
146
  if (this.child && !hasExited(this.child)) return this.status();
147
147
  this.clearChildState();
148
148
  this.stopping = false;
@@ -169,6 +169,7 @@ export class RuntimeManager {
169
169
  MINECODEX_ENABLED_FEATURES: features.join(","),
170
170
  MINECODEX_CODEX_PID_FILE: this.paths.codexPidPath,
171
171
  MINECODEX_RUNTIME_READY_FILE: this.paths.runtimeReadyPath,
172
+ MINECODEX_LAUNCH_CODEX: launchCodex ? "1" : "0",
172
173
  ...(codexExecutable ? { MINECODEX_CODEX_EXECUTABLE: codexExecutable } : {}),
173
174
  },
174
175
  stdio: "inherit",
@@ -307,7 +308,8 @@ export class RuntimeManager {
307
308
  const operation = this.enqueue(async () => {
308
309
  await this.stopInternal();
309
310
  await this.platform.terminateOwnedCodex(this.paths);
310
- return this.startInternal();
311
+ await this.platform.terminateNativeCodex?.();
312
+ return this.startInternal({ launchCodex: true });
311
313
  });
312
314
  this.restartInFlight = operation;
313
315
  const shared = this.restartInFlight.finally(() => {
@@ -141,9 +141,8 @@ 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
+ - 本地 Surface 注入前先启用 CSP bypass,并为后续 document 注册 document-start 脚本;
145
+ 当前 Codex document 始终直接注入或替换 Runtime,绝不通过 CDP reload Codex 页面。
147
146
  - 入口由 Renderer 内的 MutationObserver 幂等挂载,React 重绘不会产生重复入口。
148
147
  - Summary 根据对话主区域宽度连续派生 `overlay / shift / gutter`:小于 1096px
149
148
  使用临时 Popover;1096–1535px 预留 316px 并把对话内容左移 158px;更宽时
@@ -3570,10 +3570,6 @@ function createDocumentBootstrapSource(source) {
3570
3570
  return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] = ${CSP_BOOTSTRAP_VERSION};\n${source}`;
3571
3571
  }
3572
3572
 
3573
- function documentBootstrapExpression() {
3574
- return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] === ${CSP_BOOTSTRAP_VERSION}`;
3575
- }
3576
-
3577
3573
  async function connect(url) {
3578
3574
  const socket = new WebSocket(url);
3579
3575
  const pending = new Map();
@@ -3738,8 +3734,6 @@ export class CodexRuntime {
3738
3734
  sleep = (timeoutMs) => new Promise((resolve) => setTimeout(resolve, timeoutMs)),
3739
3735
  availabilityTimeoutMs = 20_000,
3740
3736
  availabilityPollMs = 500,
3741
- relaunchBaseDelayMs = 250,
3742
- relaunchMaxDelayMs = 2_000,
3743
3737
  monitorIntervalMs = 2_000,
3744
3738
  runtimeSessionId = randomBytes(16).toString("hex"),
3745
3739
  onStatusChange = null,
@@ -3756,8 +3750,6 @@ export class CodexRuntime {
3756
3750
  this.sleep = sleep;
3757
3751
  this.availabilityTimeoutMs = availabilityTimeoutMs;
3758
3752
  this.availabilityPollMs = availabilityPollMs;
3759
- this.relaunchBaseDelayMs = relaunchBaseDelayMs;
3760
- this.relaunchMaxDelayMs = relaunchMaxDelayMs;
3761
3753
  this.monitorIntervalMs = monitorIntervalMs;
3762
3754
  this.runtimeSessionId = runtimeSessionId;
3763
3755
  this.onStatusChange = onStatusChange;
@@ -3774,7 +3766,6 @@ export class CodexRuntime {
3774
3766
  this.targetRefreshQueued = false;
3775
3767
  this.appPid = null;
3776
3768
  this.managedCodexChild = null;
3777
- this.recoveryPromise = null;
3778
3769
  this.monitorTimer = null;
3779
3770
  this.monitorWake = null;
3780
3771
  this.stopPromise = null;
@@ -3782,20 +3773,35 @@ export class CodexRuntime {
3782
3773
  this.pidChangeQueue = null;
3783
3774
  this.rendererDiscovered = false;
3784
3775
  this.lastRendererStatus = null;
3785
- this.codexLifecycleManaged = false;
3786
3776
  }
3787
3777
 
3788
- async start() {
3778
+ async start({ launchCodex = false } = {}) {
3789
3779
  this.stopping = false;
3790
3780
  await mkdir(this.profileDir, { recursive: true });
3791
- this.codexLifecycleManaged = true;
3792
- if (!(await this.isAvailable())) {
3793
- this.launchManagedCodex();
3794
- await this.waitUntilAvailable();
3781
+ const available = await this.isAvailable();
3782
+ if (!available && launchCodex) {
3783
+ const child = this.launchManagedCodex();
3784
+ try {
3785
+ await this.waitUntilAvailable();
3786
+ } catch (error) {
3787
+ if (this.managedCodexChild === child && !processChildHasExited(child)) {
3788
+ child.kill?.("SIGTERM");
3789
+ const exited = await waitForProcessChildExit(child, 500);
3790
+ if (!exited && !processChildHasExited(child)) {
3791
+ child.kill?.("SIGKILL");
3792
+ await waitForProcessChildExit(child, 500);
3793
+ }
3794
+ }
3795
+ throw error;
3796
+ }
3795
3797
  }
3796
3798
 
3797
- await this.startTargetDiscovery();
3798
- await this.refresh();
3799
+ if (available || launchCodex) {
3800
+ await this.startTargetDiscovery();
3801
+ await this.refresh();
3802
+ } else {
3803
+ this.notifyStatusChange();
3804
+ }
3799
3805
  await this.flushManagedCodexPidChange();
3800
3806
  this.monitorPromise = this.monitor();
3801
3807
  }
@@ -3869,14 +3875,10 @@ export class CodexRuntime {
3869
3875
 
3870
3876
  markCodexUnavailable(error) {
3871
3877
  this.clearRendererState();
3872
- if (this.codexLifecycleManaged && !this.stopping) {
3873
- this.scheduleCodexRecovery();
3874
- }
3875
3878
  return error;
3876
3879
  }
3877
3880
 
3878
3881
  launchManagedCodex() {
3879
- this.codexLifecycleManaged = true;
3880
3882
  const child = this.spawnProcess(
3881
3883
  this.appPath,
3882
3884
  [
@@ -3901,7 +3903,6 @@ export class CodexRuntime {
3901
3903
  this.notifyManagedCodexPidChange(null);
3902
3904
  if (this.stopping) return;
3903
3905
  this.clearRendererState();
3904
- this.scheduleCodexRecovery();
3905
3906
  };
3906
3907
  child.once?.("exit", onExit);
3907
3908
  child.once?.("error", (error) => {
@@ -3912,73 +3913,6 @@ export class CodexRuntime {
3912
3913
  return child;
3913
3914
  }
3914
3915
 
3915
- scheduleCodexRecovery() {
3916
- if (this.stopping || this.recoveryPromise) return this.recoveryPromise;
3917
- const recovery = this.recoverManagedCodex();
3918
- let trackedRecovery;
3919
- trackedRecovery = recovery.finally(() => {
3920
- if (this.recoveryPromise === trackedRecovery) this.recoveryPromise = null;
3921
- });
3922
- this.recoveryPromise = trackedRecovery;
3923
- return trackedRecovery;
3924
- }
3925
-
3926
- async recoverManagedCodex() {
3927
- let attempt = 0;
3928
- while (!this.stopping) {
3929
- if (this.managedCodexChild && !processChildHasExited(this.managedCodexChild)) {
3930
- await this.sleep(Math.max(1, Math.min(this.relaunchMaxDelayMs, this.relaunchBaseDelayMs || 1)));
3931
- continue;
3932
- }
3933
- if (await this.isAvailable()) {
3934
- try {
3935
- await this.startTargetDiscovery();
3936
- await this.refresh();
3937
- await this.flushManagedCodexPidChange();
3938
- return;
3939
- } catch (error) {
3940
- if (this.stopping) return;
3941
- if (await this.isAvailable()) {
3942
- this.logger.warn?.("Managed Codex CDP is still available; retrying discovery", error.message);
3943
- await this.sleep(Math.max(1, Math.min(this.relaunchMaxDelayMs, this.relaunchBaseDelayMs || 1)));
3944
- continue;
3945
- }
3946
- }
3947
- }
3948
- const delay = Math.min(
3949
- this.relaunchBaseDelayMs * (2 ** attempt),
3950
- this.relaunchMaxDelayMs,
3951
- );
3952
- if (delay > 0) await this.sleep(delay);
3953
- if (this.stopping) return;
3954
- let child;
3955
- try {
3956
- child = this.launchManagedCodex();
3957
- await this.waitUntilAvailable();
3958
- await this.startTargetDiscovery();
3959
- await this.refresh();
3960
- await this.flushManagedCodexPidChange();
3961
- return;
3962
- } catch (error) {
3963
- this.logger.warn?.("Managed Codex recovery retry failed", error.message);
3964
- if (
3965
- child
3966
- && this.managedCodexChild === child
3967
- && child.exitCode === null
3968
- && child.signalCode === null
3969
- ) {
3970
- child.kill?.("SIGTERM");
3971
- const exited = await waitForProcessChildExit(child, 500);
3972
- if (!exited && !processChildHasExited(child)) {
3973
- child.kill?.("SIGKILL");
3974
- await waitForProcessChildExit(child, 500);
3975
- }
3976
- }
3977
- attempt += 1;
3978
- }
3979
- }
3980
- }
3981
-
3982
3916
  queueTargetRefresh() {
3983
3917
  if (this.targetRefreshQueued || this.stopping) return;
3984
3918
  this.targetRefreshQueued = true;
@@ -4260,20 +4194,9 @@ export class CodexRuntime {
4260
4194
  bindingToken,
4261
4195
  runtimeSessionId: this.runtimeSessionId,
4262
4196
  });
4263
- const bootstrapExpression = documentBootstrapExpression();
4264
4197
  const script = await client.send("Page.addScriptToEvaluateOnNewDocument", {
4265
4198
  source: createDocumentBootstrapSource(source),
4266
4199
  });
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
- }
4277
4200
  await waitForExpression(
4278
4201
  client,
4279
4202
  `document.readyState === "interactive" || document.readyState === "complete"`,
@@ -4303,7 +4226,6 @@ export class CodexRuntime {
4303
4226
  this.stopping = true;
4304
4227
  this.targetRefreshQueued = false;
4305
4228
  this.monitorWake?.();
4306
- await this.recoveryPromise?.catch(() => {});
4307
4229
  await this.monitorPromise?.catch(() => {});
4308
4230
  await Promise.all(Array.from(this.clients, async ([targetId, client]) => {
4309
4231
  const identifier = this.scriptIds.get(targetId);
@@ -17,6 +17,7 @@ const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot
17
17
  const legacyDataDir = process.env.CODEX_IMAGE_HOST_DATA_DIR ?? path.join(os.homedir(), ".codex-image-host");
18
18
  const profileDir = process.env.CODEX_RUNTIME_PROFILE_DIR ?? path.join(legacyDataDir, "codex-profile");
19
19
  const cdpPort = Number(process.env.CODEX_RUNTIME_CDP_PORT ?? 9231);
20
+ const launchCodex = process.env.MINECODEX_LAUNCH_CODEX === "1";
20
21
 
21
22
  const discoveredFeatures = await discoverFeatures(featuresRoot);
22
23
  if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
@@ -70,7 +71,7 @@ runtime = new CodexRuntime({
70
71
  });
71
72
 
72
73
  try {
73
- await runtime.start();
74
+ await runtime.start({ launchCodex });
74
75
  runtimeReady = true;
75
76
  await writeReadyFile();
76
77
  } catch (error) {