minecodex 0.1.1 → 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
 
@@ -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.1",
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.1",
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.1",
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.1",
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(() => {
@@ -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();
@@ -3728,8 +3738,6 @@ export class CodexRuntime {
3728
3738
  sleep = (timeoutMs) => new Promise((resolve) => setTimeout(resolve, timeoutMs)),
3729
3739
  availabilityTimeoutMs = 20_000,
3730
3740
  availabilityPollMs = 500,
3731
- relaunchBaseDelayMs = 250,
3732
- relaunchMaxDelayMs = 2_000,
3733
3741
  monitorIntervalMs = 2_000,
3734
3742
  runtimeSessionId = randomBytes(16).toString("hex"),
3735
3743
  onStatusChange = null,
@@ -3746,8 +3754,6 @@ export class CodexRuntime {
3746
3754
  this.sleep = sleep;
3747
3755
  this.availabilityTimeoutMs = availabilityTimeoutMs;
3748
3756
  this.availabilityPollMs = availabilityPollMs;
3749
- this.relaunchBaseDelayMs = relaunchBaseDelayMs;
3750
- this.relaunchMaxDelayMs = relaunchMaxDelayMs;
3751
3757
  this.monitorIntervalMs = monitorIntervalMs;
3752
3758
  this.runtimeSessionId = runtimeSessionId;
3753
3759
  this.onStatusChange = onStatusChange;
@@ -3764,7 +3770,6 @@ export class CodexRuntime {
3764
3770
  this.targetRefreshQueued = false;
3765
3771
  this.appPid = null;
3766
3772
  this.managedCodexChild = null;
3767
- this.recoveryPromise = null;
3768
3773
  this.monitorTimer = null;
3769
3774
  this.monitorWake = null;
3770
3775
  this.stopPromise = null;
@@ -3772,20 +3777,35 @@ export class CodexRuntime {
3772
3777
  this.pidChangeQueue = null;
3773
3778
  this.rendererDiscovered = false;
3774
3779
  this.lastRendererStatus = null;
3775
- this.codexLifecycleManaged = false;
3776
3780
  }
3777
3781
 
3778
- async start() {
3782
+ async start({ launchCodex = false } = {}) {
3779
3783
  this.stopping = false;
3780
3784
  await mkdir(this.profileDir, { recursive: true });
3781
- this.codexLifecycleManaged = true;
3782
- if (!(await this.isAvailable())) {
3783
- this.launchManagedCodex();
3784
- 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
+ }
3785
3801
  }
3786
3802
 
3787
- await this.startTargetDiscovery();
3788
- await this.refresh();
3803
+ if (available || launchCodex) {
3804
+ await this.startTargetDiscovery();
3805
+ await this.refresh();
3806
+ } else {
3807
+ this.notifyStatusChange();
3808
+ }
3789
3809
  await this.flushManagedCodexPidChange();
3790
3810
  this.monitorPromise = this.monitor();
3791
3811
  }
@@ -3859,14 +3879,10 @@ export class CodexRuntime {
3859
3879
 
3860
3880
  markCodexUnavailable(error) {
3861
3881
  this.clearRendererState();
3862
- if (this.codexLifecycleManaged && !this.stopping) {
3863
- this.scheduleCodexRecovery();
3864
- }
3865
3882
  return error;
3866
3883
  }
3867
3884
 
3868
3885
  launchManagedCodex() {
3869
- this.codexLifecycleManaged = true;
3870
3886
  const child = this.spawnProcess(
3871
3887
  this.appPath,
3872
3888
  [
@@ -3891,7 +3907,6 @@ export class CodexRuntime {
3891
3907
  this.notifyManagedCodexPidChange(null);
3892
3908
  if (this.stopping) return;
3893
3909
  this.clearRendererState();
3894
- this.scheduleCodexRecovery();
3895
3910
  };
3896
3911
  child.once?.("exit", onExit);
3897
3912
  child.once?.("error", (error) => {
@@ -3902,73 +3917,6 @@ export class CodexRuntime {
3902
3917
  return child;
3903
3918
  }
3904
3919
 
3905
- scheduleCodexRecovery() {
3906
- if (this.stopping || this.recoveryPromise) return this.recoveryPromise;
3907
- const recovery = this.recoverManagedCodex();
3908
- let trackedRecovery;
3909
- trackedRecovery = recovery.finally(() => {
3910
- if (this.recoveryPromise === trackedRecovery) this.recoveryPromise = null;
3911
- });
3912
- this.recoveryPromise = trackedRecovery;
3913
- return trackedRecovery;
3914
- }
3915
-
3916
- async recoverManagedCodex() {
3917
- let attempt = 0;
3918
- while (!this.stopping) {
3919
- if (this.managedCodexChild && !processChildHasExited(this.managedCodexChild)) {
3920
- await this.sleep(Math.max(1, Math.min(this.relaunchMaxDelayMs, this.relaunchBaseDelayMs || 1)));
3921
- continue;
3922
- }
3923
- if (await this.isAvailable()) {
3924
- try {
3925
- await this.startTargetDiscovery();
3926
- await this.refresh();
3927
- await this.flushManagedCodexPidChange();
3928
- return;
3929
- } catch (error) {
3930
- if (this.stopping) return;
3931
- if (await this.isAvailable()) {
3932
- this.logger.warn?.("Managed Codex CDP is still available; retrying discovery", error.message);
3933
- await this.sleep(Math.max(1, Math.min(this.relaunchMaxDelayMs, this.relaunchBaseDelayMs || 1)));
3934
- continue;
3935
- }
3936
- }
3937
- }
3938
- const delay = Math.min(
3939
- this.relaunchBaseDelayMs * (2 ** attempt),
3940
- this.relaunchMaxDelayMs,
3941
- );
3942
- if (delay > 0) await this.sleep(delay);
3943
- if (this.stopping) return;
3944
- let child;
3945
- try {
3946
- child = this.launchManagedCodex();
3947
- await this.waitUntilAvailable();
3948
- await this.startTargetDiscovery();
3949
- await this.refresh();
3950
- await this.flushManagedCodexPidChange();
3951
- return;
3952
- } catch (error) {
3953
- this.logger.warn?.("Managed Codex recovery retry failed", error.message);
3954
- if (
3955
- child
3956
- && this.managedCodexChild === child
3957
- && child.exitCode === null
3958
- && child.signalCode === null
3959
- ) {
3960
- child.kill?.("SIGTERM");
3961
- const exited = await waitForProcessChildExit(child, 500);
3962
- if (!exited && !processChildHasExited(child)) {
3963
- child.kill?.("SIGKILL");
3964
- await waitForProcessChildExit(child, 500);
3965
- }
3966
- }
3967
- attempt += 1;
3968
- }
3969
- }
3970
- }
3971
-
3972
3920
  queueTargetRefresh() {
3973
3921
  if (this.targetRefreshQueued || this.stopping) return;
3974
3922
  this.targetRefreshQueued = true;
@@ -4250,7 +4198,20 @@ export class CodexRuntime {
4250
4198
  bindingToken,
4251
4199
  runtimeSessionId: this.runtimeSessionId,
4252
4200
  });
4253
- const script = await client.send("Page.addScriptToEvaluateOnNewDocument", { source });
4201
+ const bootstrapExpression = documentBootstrapExpression();
4202
+ const script = await client.send("Page.addScriptToEvaluateOnNewDocument", {
4203
+ source: createDocumentBootstrapSource(source),
4204
+ });
4205
+ const documentWasBootstrapped = Boolean(evaluationValue(await client.send("Runtime.evaluate", {
4206
+ expression: bootstrapExpression,
4207
+ returnByValue: true,
4208
+ })));
4209
+ if (!documentWasBootstrapped) {
4210
+ const pageLoaded = client.waitFor("Page.loadEventFired", 20_000);
4211
+ await client.send("Page.reload");
4212
+ await pageLoaded;
4213
+ await waitForExpression(client, bootstrapExpression);
4214
+ }
4254
4215
  await waitForExpression(
4255
4216
  client,
4256
4217
  `document.readyState === "interactive" || document.readyState === "complete"`,
@@ -4280,7 +4241,6 @@ export class CodexRuntime {
4280
4241
  this.stopping = true;
4281
4242
  this.targetRefreshQueued = false;
4282
4243
  this.monitorWake?.();
4283
- await this.recoveryPromise?.catch(() => {});
4284
4244
  await this.monitorPromise?.catch(() => {});
4285
4245
  await Promise.all(Array.from(this.clients, async ([targetId, client]) => {
4286
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) {