minecodex 0.1.2 → 0.1.3

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.3",
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.3",
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.3",
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.3",
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(() => {
@@ -3738,8 +3738,6 @@ export class CodexRuntime {
3738
3738
  sleep = (timeoutMs) => new Promise((resolve) => setTimeout(resolve, timeoutMs)),
3739
3739
  availabilityTimeoutMs = 20_000,
3740
3740
  availabilityPollMs = 500,
3741
- relaunchBaseDelayMs = 250,
3742
- relaunchMaxDelayMs = 2_000,
3743
3741
  monitorIntervalMs = 2_000,
3744
3742
  runtimeSessionId = randomBytes(16).toString("hex"),
3745
3743
  onStatusChange = null,
@@ -3756,8 +3754,6 @@ export class CodexRuntime {
3756
3754
  this.sleep = sleep;
3757
3755
  this.availabilityTimeoutMs = availabilityTimeoutMs;
3758
3756
  this.availabilityPollMs = availabilityPollMs;
3759
- this.relaunchBaseDelayMs = relaunchBaseDelayMs;
3760
- this.relaunchMaxDelayMs = relaunchMaxDelayMs;
3761
3757
  this.monitorIntervalMs = monitorIntervalMs;
3762
3758
  this.runtimeSessionId = runtimeSessionId;
3763
3759
  this.onStatusChange = onStatusChange;
@@ -3774,7 +3770,6 @@ export class CodexRuntime {
3774
3770
  this.targetRefreshQueued = false;
3775
3771
  this.appPid = null;
3776
3772
  this.managedCodexChild = null;
3777
- this.recoveryPromise = null;
3778
3773
  this.monitorTimer = null;
3779
3774
  this.monitorWake = null;
3780
3775
  this.stopPromise = null;
@@ -3782,20 +3777,35 @@ export class CodexRuntime {
3782
3777
  this.pidChangeQueue = null;
3783
3778
  this.rendererDiscovered = false;
3784
3779
  this.lastRendererStatus = null;
3785
- this.codexLifecycleManaged = false;
3786
3780
  }
3787
3781
 
3788
- async start() {
3782
+ async start({ launchCodex = false } = {}) {
3789
3783
  this.stopping = false;
3790
3784
  await mkdir(this.profileDir, { recursive: true });
3791
- this.codexLifecycleManaged = true;
3792
- if (!(await this.isAvailable())) {
3793
- this.launchManagedCodex();
3794
- await this.waitUntilAvailable();
3785
+ const available = await this.isAvailable();
3786
+ if (!available && launchCodex) {
3787
+ const child = this.launchManagedCodex();
3788
+ try {
3789
+ await this.waitUntilAvailable();
3790
+ } catch (error) {
3791
+ if (this.managedCodexChild === child && !processChildHasExited(child)) {
3792
+ child.kill?.("SIGTERM");
3793
+ const exited = await waitForProcessChildExit(child, 500);
3794
+ if (!exited && !processChildHasExited(child)) {
3795
+ child.kill?.("SIGKILL");
3796
+ await waitForProcessChildExit(child, 500);
3797
+ }
3798
+ }
3799
+ throw error;
3800
+ }
3795
3801
  }
3796
3802
 
3797
- await this.startTargetDiscovery();
3798
- await this.refresh();
3803
+ if (available || launchCodex) {
3804
+ await this.startTargetDiscovery();
3805
+ await this.refresh();
3806
+ } else {
3807
+ this.notifyStatusChange();
3808
+ }
3799
3809
  await this.flushManagedCodexPidChange();
3800
3810
  this.monitorPromise = this.monitor();
3801
3811
  }
@@ -3869,14 +3879,10 @@ export class CodexRuntime {
3869
3879
 
3870
3880
  markCodexUnavailable(error) {
3871
3881
  this.clearRendererState();
3872
- if (this.codexLifecycleManaged && !this.stopping) {
3873
- this.scheduleCodexRecovery();
3874
- }
3875
3882
  return error;
3876
3883
  }
3877
3884
 
3878
3885
  launchManagedCodex() {
3879
- this.codexLifecycleManaged = true;
3880
3886
  const child = this.spawnProcess(
3881
3887
  this.appPath,
3882
3888
  [
@@ -3901,7 +3907,6 @@ export class CodexRuntime {
3901
3907
  this.notifyManagedCodexPidChange(null);
3902
3908
  if (this.stopping) return;
3903
3909
  this.clearRendererState();
3904
- this.scheduleCodexRecovery();
3905
3910
  };
3906
3911
  child.once?.("exit", onExit);
3907
3912
  child.once?.("error", (error) => {
@@ -3912,73 +3917,6 @@ export class CodexRuntime {
3912
3917
  return child;
3913
3918
  }
3914
3919
 
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
3920
  queueTargetRefresh() {
3983
3921
  if (this.targetRefreshQueued || this.stopping) return;
3984
3922
  this.targetRefreshQueued = true;
@@ -4303,7 +4241,6 @@ export class CodexRuntime {
4303
4241
  this.stopping = true;
4304
4242
  this.targetRefreshQueued = false;
4305
4243
  this.monitorWake?.();
4306
- await this.recoveryPromise?.catch(() => {});
4307
4244
  await this.monitorPromise?.catch(() => {});
4308
4245
  await Promise.all(Array.from(this.clients, async ([targetId, client]) => {
4309
4246
  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) {