@ailuntz/minilink 0.9.4 → 0.9.5

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
@@ -4,11 +4,13 @@
4
4
 
5
5
  ## 说明
6
6
 
7
- 官方 [Profiles](https://learn.chatgpt.com/docs/config-file/config-advanced?site_locale=en#profiles) 已支持独立配置文件:`codex --profile deep-review` 会在 `~/.codex/config.toml` 上叠加 `~/.codex/deep-review.config.toml`。配置使用顶层字段,无需嵌套在 `[profiles.deep-review]` 中。
7
+ > 官方 [Profiles](https://learn.chatgpt.com/docs/config-file/config-advanced?site_locale=en#profiles) 已支持独立配置文件:`codex --profile deep-review` 会在 `~/.codex/config.toml` 上叠加 `~/.codex/deep-review.config.toml`。配置使用顶层字段,无需嵌套在 `[profiles.deep-review]` 中。
8
8
 
9
- **截至 2026-09-24,仅添加 Profile 不会将额外供应商的模型加入 Desktop 菜单。MiniLink 读取这些配置,补充桌面端的模型选择能力。**
9
+ > **截至 2026-09-24,仅添加 Profile 不会将额外供应商的模型加入 Desktop 菜单。MiniLink 读取这些配置,补充桌面端的模型选择能力。**
10
10
 
11
11
 
12
+ > **用量提示:** 请避免耗尽原生 ChatGPT / Codex 账号的可用额度。额度耗尽后,Desktop 可能禁用输入框的发送按钮,即使切换至自定义模型也可能无法发送。MiniLink 不解除这一限制。
13
+
12
14
  ## 安装
13
15
 
14
16
  需 Node.js ≥22.16,并已安装 Desktop。
@@ -16,23 +18,27 @@
16
18
  macOS / Windows / Linux:
17
19
 
18
20
  ```sh
19
- npm install -g @ailuntz/minilink
21
+ npm install -g @ailuntz/minilink --allow-scripts=@ailuntz/minilink
20
22
  minilink on
21
23
  ```
22
24
 
23
25
  完全退出并重开 Desktop。
24
26
 
27
+ npm 11 默认可能跳过全局包的安装脚本;上面的单次许可让 MiniLink 完成 Desktop 接入,不会永久允许其他包的脚本。
28
+
25
29
  ## 命令
26
30
 
27
31
  | 命令 | 用途 |
28
32
  | --- | --- |
29
33
  | `minilink on` | 开启 |
30
34
  | `minilink link` | 选择模型与推理强度 |
31
- | `minilink off` | 关闭 |
35
+ | `minilink off` | 关闭;macOS 同时撤销后续 Desktop 启动的 MiniLink 覆盖 |
32
36
  | `minilink doctor` | 检查配置与运行状态 |
33
37
 
34
38
  通过 `minilink link` 保存选择后,完全退出并重开 Desktop。
35
39
 
40
+ macOS 上 `off` 会让后续启动直接使用 Desktop 自带的 Codex;`on` 会重新接入 MiniLink。已打开的 Desktop 需要完全退出并重开,才会切换启动方式。MiniLink 会在每次启动时重新查找 Desktop 自带的 Codex,兼容旧版 `Resources/codex` 和新版 `Resources/codex-cli/bin/codex` 布局。
41
+
36
42
  ## 配置
37
43
 
38
44
  读取 `~/.codex/*.config.toml`。通过 `model_catalog_json` 指定模型目录;未指定时继承原生目录。密钥使用提供商配置的环境变量,也可保存在 `~/.codex/.env`。
package/bin/minilink.mjs CHANGED
@@ -6,7 +6,7 @@ import { startUpdate, updateStatus } from '../src/update.mjs';
6
6
  import { runProxy } from '../src/proxy.mjs';
7
7
  import { pickerEndpoints } from '../src/picker.mjs';
8
8
  import { selectModel, text, language } from '../src/terminal.mjs';
9
- import { installation, environmentStatus } from '../src/install.mjs';
9
+ import { installation, environmentStatus, syncLaunchEnvironment } from '../src/install.mjs';
10
10
 
11
11
  const self = fileURLToPath(import.meta.url), args = process.argv.slice(2);
12
12
  const commands = ['on', 'link', 'off', 'doctor'];
@@ -21,6 +21,7 @@ async function main() {
21
21
  if (command === 'on' || command === 'off') {
22
22
  if (command === 'on') discoverProfiles();
23
23
  atomicJSON(configPath(), { version: 1, enabled: command === 'on' });
24
+ syncLaunchEnvironment({ enabled: command === 'on' });
24
25
  return output(text(command));
25
26
  }
26
27
  if (command === 'link') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ailuntz/minilink",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Use your Codex profiles in the Desktop model menu.",
5
5
  "type": "module",
6
6
  "license": "MIT AND Apache-2.0",
package/src/config.mjs CHANGED
@@ -63,13 +63,27 @@ export function discoverProfiles(home = codexHome()) {
63
63
  return routes;
64
64
  }
65
65
 
66
+ export const bundledCodexPaths = [
67
+ 'codex-cli/bin/codex',
68
+ 'codex-cli/CodexCLI.app/Contents/MacOS/codex',
69
+ 'codex'
70
+ ];
71
+
72
+ export function bundledCodex(app) {
73
+ for (const relative of bundledCodexPaths) {
74
+ const candidate = join(app, 'Contents/Resources', relative);
75
+ try { accessSync(candidate, constants.X_OK); return candidate; } catch {}
76
+ }
77
+ return null;
78
+ }
79
+
66
80
  export function findApp() {
67
- return [process.env.CODEX_DESKTOP_APP, '/Applications/ChatGPT.app', '/Applications/Codex.app', join(homedir(), 'Applications/ChatGPT.app'), join(homedir(), 'Applications/Codex.app')].filter(Boolean).find(p => existsSync(join(p, 'Contents/Resources/codex')));
81
+ return [process.env.CODEX_DESKTOP_APP, '/Applications/ChatGPT.app', '/Applications/Codex.app', join(homedir(), 'Applications/ChatGPT.app'), join(homedir(), 'Applications/Codex.app')].filter(Boolean).find(p => bundledCodex(p));
68
82
  }
69
83
 
70
84
  export function resolveRealCodex(selfPath) {
71
85
  const app = findApp();
72
- const candidate = setting('REAL_CODEX') || (app && join(app, 'Contents/Resources/codex'));
86
+ const candidate = setting('REAL_CODEX') || (app && bundledCodex(app));
73
87
  if (!candidate || !isAbsolute(candidate)) throw new Error('找不到内置 Codex;可用 MINILINK_REAL_CODEX 指定绝对路径。');
74
88
  let actual;
75
89
  try { actual = realpathSync(candidate); accessSync(actual, constants.X_OK); } catch { throw new Error('真正的 Codex 不存在或不可执行。'); }
package/src/install.mjs CHANGED
@@ -5,7 +5,7 @@ import { execFileSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { activeRuns } from './runtime.mjs';
7
7
  import { legacyInstallation } from './legacy.mjs';
8
- import { atomicJSON, configPath, codexHome, resolveRealCodex } from './config.mjs';
8
+ import { atomicJSON, configPath, codexHome, resolveRealCodex, readConfig, bundledCodexPaths } from './config.mjs';
9
9
 
10
10
  const label = 'com.ailuntz.minilink';
11
11
  export const installRoot = () => join(homedir(), '.local/share/minilink');
@@ -18,6 +18,22 @@ export function installation(root = installRoot()) {
18
18
  try { return JSON.parse(readFileSync(join(root, 'installation.json'), 'utf8')); } catch (e) { if (e.code === 'ENOENT') return null; throw new Error('安装状态损坏,请检查 installation.json。'); }
19
19
  }
20
20
 
21
+ // A disabled integration must not remain in Desktop's launch path. The login
22
+ // agent reapplies this state in the Aqua session when the command ran over SSH.
23
+ export function syncLaunchEnvironment({ state = installation(), run = launchctl, enabled, previousWrapper } = {}) {
24
+ if (process.platform !== 'darwin' || !state?.wrapper) return;
25
+ if (enabled == null) {
26
+ try { enabled = readConfig(state.configPath).enabled; } catch { enabled = false; }
27
+ }
28
+ const current = run(['getenv', 'CODEX_CLI_PATH'], false) || null;
29
+ if (enabled) {
30
+ if (!current || current === previousWrapper) run(['setenv', 'CODEX_CLI_PATH', state.wrapper]);
31
+ } else if (current === state.wrapper || current === previousWrapper) {
32
+ if (state.previousEnv) run(['setenv', 'CODEX_CLI_PATH', state.previousEnv]);
33
+ else run(['unsetenv', 'CODEX_CLI_PATH']);
34
+ }
35
+ }
36
+
21
37
  function atomicFile(path, text, mode) {
22
38
  const temporary = `${path}.${process.pid}.tmp`;
23
39
  try { writeFileSync(temporary, text, { mode }); renameSync(temporary, path); }
@@ -62,15 +78,19 @@ export function install({ root = installRoot(), plist = defaultPlist(), source =
62
78
  const state = { version: 2, source, runtime, wrapper, node, npm: npm || previous?.npm, prefix: prefix || previous?.prefix, codexHome: home, configPath: config, previousEnv: previous?.previousEnv || legacy?.state.previousEnv || null, installedAt: new Date().toISOString() };
63
79
  // The small fallback also serves Desktop instances that inherited the old
64
80
  // launch environment before npm uninstall removed the integration.
65
- atomicFile(wrapper, `#!/bin/sh\nif [ ! -f ${shellQuote(stateFile)} ]; then exec ${shellQuote(realCodex)} "$@"; fi\nexport CODEX_HOME=${shellQuote(home)}\nexport MINILINK_CONFIG=${shellQuote(config)}\nexec ${shellQuote(node)} ${shellQuote(join(runtime, 'bin/minilink.mjs'))} "$@"\n`, 0o755);
81
+ const fallbackApps = ['/Applications/ChatGPT.app', '/Applications/Codex.app', join(homedir(), 'Applications/ChatGPT.app'), join(homedir(), 'Applications/Codex.app')];
82
+ const fallback = `if [ ! -f ${shellQuote(stateFile)} ]; then\n if [ -n "$MINILINK_REAL_CODEX" ] && [ -x "$MINILINK_REAL_CODEX" ]; then exec "$MINILINK_REAL_CODEX" "$@"; fi\n for app in "$CODEX_DESKTOP_APP" ${fallbackApps.map(shellQuote).join(' ')}; do\n [ -n "$app" ] || continue\n for rel in ${bundledCodexPaths.map(shellQuote).join(' ')}; do\n candidate="$app/Contents/Resources/$rel"\n if [ -x "$candidate" ]; then exec "$candidate" "$@"; fi\n done\n done\n exec ${shellQuote(realCodex)} "$@"\nfi`;
83
+ atomicFile(wrapper, `#!/bin/sh\n${fallback}\nexport CODEX_HOME=${shellQuote(home)}\nexport MINILINK_CONFIG=${shellQuote(config)}\nexec ${shellQuote(node)} ${shellQuote(join(runtime, 'bin/minilink.mjs'))} "$@"\n`, 0o755);
66
84
  atomicFile(activate, `#!/bin/sh\nexec ${shellQuote(node)} ${shellQuote(join(runtime, 'src/lifecycle.mjs'))}\n`, 0o700);
67
85
  mkdirSync(dirname(plist), { recursive: true });
68
86
  atomicFile(plist, `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array><string>/bin/sh</string><string>${xml(activate)}</string></array><key>RunAtLoad</key><true/><key>StartInterval</key><integer>30</integer><key>LimitLoadToSessionType</key><string>Aqua</string></dict></plist>\n`, 0o600);
69
87
  atomicJSON(stateFile, state);
70
88
  run(['bootout', `gui/${process.getuid()}/${label}`], false);
71
89
  run(['bootstrap', `gui/${process.getuid()}`, plist]);
72
- run(['setenv', 'CODEX_CLI_PATH', wrapper]);
73
- if (run(['getenv', 'CODEX_CLI_PATH'], false) !== wrapper) throw new Error('启动环境验证失败。');
90
+ let enabled = false;
91
+ try { enabled = readConfig(config).enabled; } catch {}
92
+ syncLaunchEnvironment({ state, run, enabled, previousWrapper: legacy?.state.wrapper });
93
+ if (enabled && run(['getenv', 'CODEX_CLI_PATH'], false) !== wrapper) throw new Error('启动环境验证失败。');
74
94
  if (legacy) {
75
95
  run(['bootout', `gui/${process.getuid()}/${legacy.label}`], false); retired = true;
76
96
  atomicFile(legacy.state.wrapper, `#!/bin/sh\nexec ${shellQuote(wrapper)} "$@"\n`, 0o755);
@@ -117,8 +137,7 @@ export function reconcile({ root = installRoot(), plist = defaultPlist(), run =
117
137
  if (existsSync(join(state.source, 'package.json')) || busy(root)) {
118
138
  if (!busy(root)) pruneReleases(root, state, now);
119
139
  rmSync(marker, { force: true });
120
- const env = run(['getenv', 'CODEX_CLI_PATH'], false);
121
- if (!env) run(['setenv', 'CODEX_CLI_PATH', state.wrapper]);
140
+ syncLaunchEnvironment({ state, run });
122
141
  return;
123
142
  }
124
143
  let since;
package/src/proxy.mjs CHANGED
@@ -34,12 +34,12 @@ export function isStdioServer(args) {
34
34
  export async function runProxy(args, selfPath) {
35
35
  if ((Number.parseInt(setting('ACTIVE'), 10) || 0) >= 8) throw new Error('检测到 minilink 递归调用;MINILINK_REAL_CODEX 必须指向真正的 codex。');
36
36
  const path = configPath();
37
- readConfig(path);
37
+ const enabled = readConfig(path).enabled;
38
38
  const hadConfig = existsSync(path);
39
39
  const executable = resolveRealCodex(selfPath);
40
- const routed = isStdioServer(args);
40
+ const routed = enabled && isStdioServer(args);
41
41
  const routes = routed ? discoverProfiles() : [];
42
- const env = childEnvironment(routes);
42
+ const env = enabled ? childEnvironment(routes) : { ...process.env, MINILINK_ACTIVE: String((Number.parseInt(setting('ACTIVE'), 10) || 0) + 1) };
43
43
  env.CODEX_CLI_PATH = executable;
44
44
  const chats = routes.filter(r => r.chat).map(route => new Chat({ config: route, key: env[route.providerDefinition.env_key], env }));
45
45
  const picker = routed ? new Picker({ chats }) : null;
package/src/update.mjs CHANGED
@@ -61,7 +61,7 @@ export async function upgrade(latest, state, { root = installRoot(), exec = exec
61
61
  // A manual uninstall during the download must not be undone by the worker.
62
62
  if (!existsSync(state.source) || installation(root)?.installedAt !== state.installedAt) return false;
63
63
  installing = true;
64
- await npm(['install', '-g', '--prefix', state.prefix, candidate, '--offline', '--engine-strict', '--ignore-scripts=false']);
64
+ await npm(['install', '-g', '--prefix', state.prefix, candidate, '--offline', '--engine-strict', `--allow-scripts=${name}`]);
65
65
  const active = installation(root);
66
66
  if (!active?.runtime || JSON.parse(readFileSync(join(active.runtime, 'package.json'), 'utf8')).version !== latest) throw new Error('Desktop activation failed');
67
67
  return true;