minecodex 2.0.40 → 2.0.41

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
@@ -35,7 +35,9 @@ mcx open
35
35
 
36
36
  安装后重启 Codex 即可使用。在控制台按需开关功能,修改后运行 `mcx restart` 应用设置。
37
37
 
38
- 如需在 Codex 内打开控制台和 Prompt 页面,先安装 [Codex CLI](https://developers.openai.com/codex/cli/),再运行 `mcx plugin install`,从 Explore/探索中打开。
38
+ 如需在 Codex 内打开控制台和 Prompt 页面,先安装 [Codex CLI](https://developers.openai.com/codex/cli/),再运行 `mcx plugin install`。这一步独立于后台增强安装;更换全局包或源码安装位置后,再运行一次以更新插件路径。
39
+
40
+ 注册后,在 Codex 的插件页点击刷新,再从 Explore/探索找到 MineCodex 和 Prompt。需要常驻 Navbar 时,分别点击它们旁边的“固定到侧边栏”。插件已安装、页面入口已发现、入口已固定是不同状态;未固定的入口不会常驻 Navbar。此路径已在 Codex 26.1013.4894.0 的 Windows 简体中文窗口验证。
39
41
 
40
42
  ## 常用命令
41
43
 
@@ -13,6 +13,8 @@ let disposed = false;
13
13
 
14
14
  function isUpsellBanner(element) {
15
15
  if (!(element instanceof window.HTMLElement)) return false;
16
+ // 文件侧栏也是 aside,正文中的用量说明不能使整个预览被隐藏。
17
+ if (element.querySelector("[role='tabpanel']")) return false;
16
18
  const text = (element.textContent ?? "").trim();
17
19
  if (!text) return false;
18
20
  const title = (
@@ -25,6 +25,7 @@ export function quickChatLifecycleExports(source) {
25
25
  }
26
26
 
27
27
  export function installQuickChatShortcut(signal, codex, discover = null) {
28
+ const windows = navigator.platform.startsWith('Win');
28
29
  let bindings = null, disposed = false, pendingFocus = null;
29
30
  function activateExisting() {
30
31
  // 原生恢复按钮同时恢复 presentation、取消最小化并聚焦输入框。
@@ -97,8 +98,9 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
97
98
  codex.report('quick-chat-shortcut', true);
98
99
  }).catch(() => { if (!disposed && !signal.aborted) codex.report('quick-chat-shortcut', false, 'native-shortcut-contract-unavailable'); });
99
100
  function keydown(event) {
101
+ const primaryModifier = windows ? event.ctrlKey && !event.metaKey : event.metaKey && !event.ctrlKey;
100
102
  if (disposed || signal.aborted || bindings?.native || event.defaultPrevented || event.repeat || event.isComposing
101
- || !event.metaKey || !event.altKey || event.ctrlKey || event.shiftKey || event.code !== 'KeyN') return;
103
+ || !primaryModifier || !event.altKey || event.shiftKey || event.code !== 'KeyN') return;
102
104
  const scope = bindings && context();
103
105
  if (scope && bindings.isOpen(scope) === false) {
104
106
  pendingFocus={scope,previous:new Set([...document.querySelectorAll('[data-floating-chat-surface]')].map(sessionId))};
@@ -38,12 +38,17 @@ export function createContextUsageAdapter(signal, codex, onChange, loadFamily =
38
38
  }
39
39
  async function discover() {
40
40
  if (loadFamily) return loadFamily();
41
- const url=[...document.querySelectorAll('link[rel=modulepreload]')].map(link=>link.href).find(url=>/\/app-shared-[^/]+\.js(?:$|\?)/.test(url));
42
- if (!url) throw new Error("context-bundle-unavailable");
43
- const source=await (await fetch(url,{signal})).text();
44
- const exported=contextUsageExport(source);
45
- if (!exported) throw new Error("context-export-unavailable");
46
- return (await import(url))[exported];
41
+ const urls=[...document.querySelectorAll('link[rel=modulepreload],script[src]')].map(node=>node.href || node.src);
42
+ const initial=urls.find(url=>/\/app-initial-[^/]+\.js(?:$|\?)/.test(url));
43
+ const shared=urls.find(url=>/\/app-shared-[^/]+\.js(?:$|\?)/.test(url));
44
+ if (!initial && !shared) throw new Error("context-bundle-unavailable");
45
+ // 当前 Codex 将上下文接口放在 app-initial,按实际导出识别。
46
+ for (const url of [initial,shared].filter(Boolean)) {
47
+ const source=await (await fetch(url,{signal})).text();
48
+ const exported=contextUsageExport(source);
49
+ if (exported) return (await import(url))[exported];
50
+ }
51
+ throw new Error("context-export-unavailable");
47
52
  }
48
53
  void discover().then(value=>{ if (!signal.aborted) { family=value; connect(thread,composer); } })
49
54
  .catch(()=>codex.report("context-usage",false,"context-capability-unavailable"));
@@ -1,13 +1,18 @@
1
1
  export function createFocusAdapter(codex) {
2
2
  const sectionSelector="[data-app-action-sidebar-section]", projectSelector="[data-app-action-sidebar-project-row]";
3
3
  const props = (element,predicate) => codex.props(element,predicate,30) ?? {};
4
+ const visible = element => element.checkVisibility({visibilityProperty:true});
5
+ function activity() {
6
+ return [...document.querySelectorAll('#app-shell-sidebar button[aria-pressed]')]
7
+ .find(element => visible(element) && /activity|活动|动态/i.test(element.getAttribute("aria-label") || ""));
8
+ }
4
9
  function branches() {
5
- return [...document.querySelectorAll("#app-shell-sidebar " + sectionSelector)].map(element => {
10
+ return [...document.querySelectorAll("#app-shell-sidebar " + sectionSelector)].filter(visible).map(element => {
6
11
  const native = props(element, p => !!p.sectionKey);
7
12
  const data = props(element, p => Array.isArray(p.section?.threadKeys)).section;
8
13
  return { element, key:"section:" + native.sectionKey, data, expanded:element.dataset.appActionSidebarSectionCollapsed === "false",
9
14
  toggle:() => native.onCollapsedChange?.(element.dataset.appActionSidebarSectionCollapsed === "false") };
10
- }).concat([...document.querySelectorAll("#app-shell-sidebar " + projectSelector)].map(element => ({
15
+ }).concat([...document.querySelectorAll("#app-shell-sidebar " + projectSelector)].filter(visible).map(element => ({
11
16
  element, key:"project:" + element.dataset.appActionSidebarProjectId,
12
17
  data:props(element, p => !!p.group?.projectId).group,
13
18
  expanded:element.getAttribute("aria-expanded") === "true",
@@ -15,14 +20,15 @@ function branches() {
15
20
  })));
16
21
  }
17
22
  function target(items) {
18
- const selected = document.querySelector('#app-shell-sidebar [data-app-action-sidebar-thread-selected="true"]');
23
+ // 两种模式的侧栏同时挂载,只在当前分支中读取原生选中身份。
24
+ const selected = items.map(b => b.element.querySelector?.('[data-app-action-sidebar-thread-selected="true"], [data-sidebar-chatgpt-conversation-key]:has([aria-current="page"])')).find(Boolean);
19
25
  const id = codex.currentThreadId();
20
- const key = selected?.dataset.appActionSidebarThreadId || (id ? "local:" + id : null);
26
+ const key = selected?.dataset.appActionSidebarThreadId || selected?.dataset.sidebarChatgptConversationKey || (id ? "local:" + id : null);
21
27
  const activeProject = items.map(b => props(b.element, p => p.activeProjectId != null).activeProjectId).find(Boolean);
22
28
  const project = items.find(b => b.key.startsWith("project:") && (b.data?.threadKeys?.includes(key) || b.data?.projectId === activeProject));
23
29
  const section = items.find(b => b.key.startsWith("section:") && (b.data?.threadKeys?.includes(key) || (activeProject && b.data?.itemKeys?.includes("codex:project:" + activeProject))))
24
30
  || items.find(b => b.key.startsWith("section:") && (selected && b.element.contains(selected) || project && b.element.contains(project.element)));
25
31
  return section ? { section, project } : null;
26
32
  }
27
- return {branches,target,isBranchToggle:element=>!!element.closest("[data-app-action-sidebar-section-toggle], " + projectSelector)};
33
+ return {activity,branches,target,isBranchToggle:element=>!!element.closest("[data-app-action-sidebar-section-toggle], " + projectSelector)};
28
34
  }
@@ -4,7 +4,7 @@ const EXPAND_GLYPH = "<svg aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/200
4
4
 
5
5
 
6
6
  const states = new Map();
7
- let active = false, restoring = false, button, timer = null, disposed = false;
7
+ let active = false, restoring = false, button, currentBell, timer = null, disposed = false;
8
8
  const text = (en, cn) => (document.documentElement.lang || navigator.language).startsWith("zh") ? cn : en;
9
9
 
10
10
 
@@ -15,8 +15,10 @@ function remember(items) {
15
15
  function reconcile() {
16
16
  if (disposed) return;
17
17
  const sidebar = document.querySelector("#app-shell-sidebar");
18
- const bell = [...(sidebar?.querySelectorAll("button[aria-pressed]") || [])].find(e => /activity|活动|动态/.test(e.getAttribute("aria-label") || ""));
18
+ const bell = nativeFocus.activity();
19
19
  if (!bell) { button?.remove(); button = null; return; }
20
+ if (currentBell !== bell) { button?.remove(); button = null; }
21
+ currentBell = bell;
20
22
  if (!button?.isConnected) {
21
23
  button = bell.cloneNode(false);
22
24
  button.removeAttribute("id");
@@ -93,7 +95,7 @@ const observer = new MutationObserver(records => {
93
95
  )) schedule();
94
96
  });
95
97
  observer.observe(document.documentElement, { subtree:true, childList:true, attributes:true, attributeFilter:[
96
- "aria-expanded", "aria-pressed", "data-app-action-sidebar-thread-selected",
98
+ "aria-expanded", "aria-pressed", "aria-current", "style", "class", "data-app-action-sidebar-thread-selected",
97
99
  "data-app-action-sidebar-section-collapsed", "data-above-composer-conversation-id", "lang"
98
100
  ] });
99
101
  reconcile();
@@ -34,7 +34,7 @@ function pageInfo() {
34
34
  const ownershipTabs=[...page.querySelectorAll('[role="tablist"]')].find(node=>!node.closest(`[${owned}]`)&&nativeProps(node,p=>p.tabs?.some(tab=>tab.key==="owned")&&p.tabs?.some(tab=>tab.key==="shared")));
35
35
  const ownership=ownershipTabs&&nativeProps(ownershipTabs,p=>p.tabs?.some(tab=>tab.key==="owned")&&p.tabs?.some(tab=>tab.key==="shared"));
36
36
  const shared = ownership ? ownership.selectedKey === "shared" : native ? !!native.shared : [...page.querySelectorAll("button[aria-pressed=true]")].some(b => /Shared with you|与你共享|與你共用/.test(b.textContent));
37
- const create = [...(plus ? page : page.firstElementChild).querySelectorAll("button")].find(b => visible(b) && (plus ? /^(Create Site|创建站点|建立網站)$/ : /^(New|新建|新增)$/).test(b.textContent.trim()) && !b.hasAttribute(owned));
37
+ const create = [...(plus ? page : page.firstElementChild).querySelectorAll("button")].find(b => visible(b) && (plus ? /^(Create Site|创建站点|创建新站点|建立網站)$/ : /^(New|新建|新增)$/).test(b.textContent.trim()) && !b.hasAttribute(owned));
38
38
  codex.report("sites-page",true);
39
39
  return { page, input, content, list, grid, shared, create, plus, ownershipTabs:plus?(ownershipTabs??null):null, accountKey:account.read().key, empty: list ? null : [...content.children].findLast(e => !e.hasAttribute(owned)) };
40
40
  }
@@ -36,6 +36,7 @@ function setButtonDisabled(button, disabled) {
36
36
  button.disabled = disabled; button.setAttribute("aria-disabled", String(disabled)); button.toggleAttribute("data-disabled", disabled);
37
37
  }
38
38
  function notice(message) {
39
+ if (disposed || signal.aborted) return;
39
40
  document.querySelector(`[${owned}=notice]`)?.remove();
40
41
  const n = element("div", "", message); n.setAttribute(owned, "notice"); n.setAttribute("role", "status"); document.body.append(n);
41
42
  setTimeout(() => n.remove(), 7000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.40",
3
+ "version": "2.0.41",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,9 +25,10 @@ export function resolvePaths({ homeDir = os.homedir(), platform = process.platfo
25
25
  supportDir,
26
26
  logsDir,
27
27
  serviceDir,
28
- // Windows 无 .app 服务包;服务入口是计划任务拉起的 cmd 脚本。
28
+ // Windows 通过隐藏启动器等待 cmd 服务,保留计划任务的退出状态。
29
29
  serviceBundleSource: null,
30
30
  serviceAppPath: winPath.join(serviceDir, "MineCodexService.cmd"),
31
+ serviceLauncherPath: winPath.join(serviceDir, "MineCodexService-hidden.vbs"),
31
32
  serviceManagerPath: null,
32
33
  serviceRuntimeConfigPath: winPath.join(supportDir, "service-runtime.json"),
33
34
  configPath: winPath.join(supportDir, "config.json"),
@@ -217,6 +217,20 @@ export function buildWindowsServiceCommand({ nodePath, cliPath, paths }) {
217
217
  ].join("\r\n");
218
218
  }
219
219
 
220
+ export function buildWindowsServiceLauncher(paths) {
221
+ const serviceCmd = String(paths.serviceAppPath).replaceAll('"', '""');
222
+ return [
223
+ "Option Explicit",
224
+ "Dim shell, command, exitCode",
225
+ 'Set shell = CreateObject("WScript.Shell")',
226
+ "' 隐藏窗口并等待服务退出,向计划任务传递真实退出码。",
227
+ 'command = Chr(34) & shell.ExpandEnvironmentStrings("%ComSpec%") & Chr(34) & " /d /s /c " & Chr(34) & Chr(34) & "' + serviceCmd + '" & Chr(34) & Chr(34)',
228
+ "exitCode = shell.Run(command, 0, True)",
229
+ "WScript.Quit exitCode",
230
+ "",
231
+ ].join("\r\n");
232
+ }
233
+
220
234
  // cmd 的引号转义是内部双引号翻倍,与 JSON 转义规则不同,不能混用。
221
235
  function cmdQuote(value) {
222
236
  return '"' + String(value).replaceAll('"', '""') + '"';
@@ -907,6 +921,8 @@ export class WindowsPlatformAdapter {
907
921
 
908
922
  async writeServiceCommand(paths, content) {
909
923
  await this.fileSystem.writeFile(paths.serviceAppPath, content, { mode: 0o700 });
924
+ // WSH 用 UTF-16 BOM 识别中文路径,避免依赖系统 ANSI 代码页。
925
+ await this.fileSystem.writeFile(paths.serviceLauncherPath, Buffer.from("\uFEFF" + buildWindowsServiceLauncher(paths), "utf16le"), { mode: 0o700 });
910
926
  }
911
927
 
912
928
  async snapshotServiceState(paths) {
@@ -956,8 +972,9 @@ export class WindowsPlatformAdapter {
956
972
  async registerServiceTask(paths) {
957
973
  // ExecutionTimeLimit 0 表示不限时:服务是常驻进程,不能被默认 72h 限制回收。
958
974
  const script = [
959
- "$serviceCmd = '" + String(paths.serviceAppPath).replaceAll("'", "''") + "';",
960
- '$action = New-ScheduledTaskAction -Execute \'cmd.exe\' -Argument (\'/c ""\' + $serviceCmd + \'""\');',
975
+ "$launcher = '" + String(paths.serviceLauncherPath).replaceAll("'", "''") + "';",
976
+ "$serviceDir = '" + String(paths.serviceDir).replaceAll("'", "''") + "';",
977
+ '$action = New-ScheduledTaskAction -Execute (Join-Path $env:SystemRoot \'System32\\wscript.exe\') -Argument (\'//B //Nologo "\' + $launcher + \'"\') -WorkingDirectory $serviceDir;',
961
978
  "$trigger = New-ScheduledTaskTrigger -AtLogOn;",
962
979
  "$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1);",
963
980
  "Register-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null;",
@@ -1179,18 +1196,32 @@ export class WindowsPlatformAdapter {
1179
1196
 
1180
1197
  async openNativeCodex({ cdpPort } = {}) {
1181
1198
  const installation = await this.codex();
1182
- if (!Number.isInteger(cdpPort) || cdpPort <= 0 || cdpPort > 65_535) {
1183
- await this.powershell("Start-Process -FilePath '" + String(installation.executable).replaceAll("'", "''") + "'");
1184
- return null;
1185
- }
1186
- const existingPids = new Set((await this.listCodexProcesses()).map(({ pid }) => pid));
1187
- const safeExecutable = String(installation.executable).replaceAll("'", "''");
1199
+ const withCdp = Number.isInteger(cdpPort) && cdpPort > 0 && cdpPort <= 65_535;
1200
+ const existingPids = new Set(withCdp ? (await this.listCodexProcesses()).map(({ pid }) => pid) : []);
1201
+ const appId = `${installation.packageFamilyName}!App`.replaceAll("'", "''");
1202
+ const args = withCdp
1203
+ ? `--remote-debugging-address=127.0.0.1 --remote-debugging-port=${cdpPort} --remote-allow-origins=http://127.0.0.1:${cdpPort}`
1204
+ : "";
1205
+ // 通过已注册应用入口激活,保留任务栏身份及扩展所需参数。
1188
1206
  await this.powershell([
1189
- "Start-Process -FilePath '" + safeExecutable + "' -ArgumentList @(",
1190
- " '--remote-debugging-address=127.0.0.1',",
1191
- " '--remote-debugging-port=" + cdpPort + "',",
1192
- " '--remote-allow-origins=http://127.0.0.1:" + cdpPort + "');",
1207
+ "Add-Type -TypeDefinition @'",
1208
+ "using System;",
1209
+ "using System.Runtime.InteropServices;",
1210
+ "public static class MineCodexRegisteredLaunch {",
1211
+ " [ComImport, Guid(\"2E941141-7F97-4756-BA1D-9DECDE894A3D\"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]",
1212
+ " interface Activation {",
1213
+ " [PreserveSig] int ActivateApplication([MarshalAs(UnmanagedType.LPWStr)] string id, [MarshalAs(UnmanagedType.LPWStr)] string args, uint options, out uint pid);",
1214
+ " }",
1215
+ " public static uint Launch(string id, string args) {",
1216
+ " object manager = Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid(\"45BA127D-10A8-46EA-8AB7-56EA9078943C\")));",
1217
+ " try { uint pid; Marshal.ThrowExceptionForHR(((Activation)manager).ActivateApplication(id, args, 0, out pid)); return pid; }",
1218
+ " finally { Marshal.ReleaseComObject(manager); }",
1219
+ " }",
1220
+ "}",
1221
+ "'@",
1222
+ `[MineCodexRegisteredLaunch]::Launch('${appId}', '${args}') | Out-Null`,
1193
1223
  ].join("\n"));
1224
+ if (!withCdp) return null;
1194
1225
  return this.waitForNativeCodex({ cdpPort, excludedPids: existingPids });
1195
1226
  }
1196
1227
 
@@ -13,16 +13,19 @@ export async function loadNativeMarkdownRenderer(source, url, module) {
13
13
  ].find((m) => m[2].includes(`${init}()`) && binding(m[1]));
14
14
  if (!initializer || !binding(markdownName)) throw new Error('native-rich-markdown-contract-unavailable');
15
15
  binding(initializer[1])();
16
- let lexer, sharedUrl;
17
16
  const lexerName = source.match(/tokens:([\w$]+)\(e,void 0\)/)?.[1];
18
- for (const m of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
17
+ let lexer = lexerName ? binding(lexerName) : undefined, sharedUrl;
18
+ // lexer 可在入口模块本地导出;代码组件仍从独立的共享模块加载。
19
+ for (const m of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)) {
20
+ const importUrl = new URL(m[2], url).href;
21
+ if (/\/app-shared-[^/]+\.(?:js|mjs)$/.test(importUrl)) sharedUrl = importUrl;
19
22
  for (const entry of m[1].split(',')) {
20
23
  const parts = entry.trim().split(/\s+as\s+/);
21
24
  if (parts.at(-1) === lexerName) {
22
- sharedUrl = new URL(m[2], url).href;
23
- lexer = (await import(sharedUrl))[parts[0]];
25
+ lexer = (await import(importUrl))[parts[0]];
24
26
  }
25
27
  }
28
+ }
26
29
  if (!sharedUrl || typeof lexer !== 'function') throw new Error('native-markdown-lexer-unavailable');
27
30
  return { Markdown: binding(markdownName), lexer, sharedUrl };
28
31
  }
@@ -69,7 +69,7 @@ export async function loadNativeMarkdownToc() {
69
69
  )?.[1];
70
70
  const Tooltip = await binding(tooltipLocal);
71
71
  const layoutUrl = [...imports.values()].find((item) =>
72
- item.url.includes('/floating-navigation-rail-layout-'),
72
+ /\/floating-navigation-rail-(?:layout-)?[\w-]+\.js(?:$|\?)/.test(item.url),
73
73
  )?.url;
74
74
  if (!layoutUrl) throw new Error('native-page-toc-layout-unavailable');
75
75
  const layout = await import(layoutUrl);
@@ -84,7 +84,7 @@ export async function loadNativeMarkdownToc() {
84
84
  mainImports.set(parts.at(-1), { name: parts[0], url: new URL(match[2], mainUrl).href });
85
85
  }
86
86
  const ReactDOM = (await binding(rootFactory, mainImports))();
87
- const css = initial.match(/["']\.\/((?:floating-navigation-rail-layout)-[\w-]+\.css)["']/)?.[1];
87
+ const css = initial.match(/["']\.\/((?:floating-navigation-rail(?:-layout)?)-[\w-]+\.css)["']/)?.[1];
88
88
  if (!css) throw new Error('native-page-toc-styles-unavailable');
89
89
  const cssUrl = new URL(css, initialUrl).href;
90
90
  if (![...document.querySelectorAll('link[rel="stylesheet"]')].some((n) => n.href === cssUrl)) {
@@ -114,9 +114,10 @@ export async function loadNativeMarkdownToc() {
114
114
  const classes = {
115
115
  rail: requireClass(railBody, /ref:[\w$]+,className:`([^`]+)`,"data-floating-navigation-rail-list"/),
116
116
  railButton: requireClass(railBody, /className:`([^`]+)`,"aria-current"/),
117
- menu: requireClass(listBody, /ref:[\w$]+,className:`([^`]+)`,children:/),
117
+ menu: requireClass(listBody, /ref:[\w$]+,className:`([^`]+)`,(?:variant:`viewer`,)?children:/),
118
118
  menuButton: requireClass(listBody, /className:[\w$]+\(`([^`]+)`/),
119
- nav: requireClass(source, /className:`(absolute top-1\/2 left-1\.5[^`]+)`/),
119
+ // 按 TOC 调用角色找导航样式,避免绑定随布局变化的 top utility。
120
+ nav: requireClass(bodyMatching(/skipDelayKey:`page-table-of-contents`/), /jsx\)\(`nav`,\{"aria-label":[\w$]+,className:`([^`]+)`/),
120
121
  };
121
122
  const indentName = listBody.match(/`block`,([\w$]+)\[e\.level\]/)?.[1];
122
123
  const indentValue = indentName && source.match(new RegExp(`\\b${indentName}=(\\[[^\\]]+\\])`))?.[1];