@ailuntz/minilink 0.9.4 → 0.9.6
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 +10 -4
- package/bin/minilink.mjs +2 -1
- package/package.json +1 -1
- package/src/config.mjs +6 -2
- package/src/install.mjs +26 -9
- package/src/proxy.mjs +3 -3
- package/src/update.mjs +1 -1
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 使用 ChatGPT 当前内置的 `Resources/codex-cli/CodexCLI.app/Contents/MacOS/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
package/src/config.mjs
CHANGED
|
@@ -63,13 +63,17 @@ export function discoverProfiles(home = codexHome()) {
|
|
|
63
63
|
return routes;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export const bundledCodexRelativePath = 'Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex';
|
|
67
|
+
export const bundledCodex = app => join(app, bundledCodexRelativePath);
|
|
68
|
+
|
|
66
69
|
export function findApp() {
|
|
67
|
-
|
|
70
|
+
const app = process.env.CODEX_DESKTOP_APP || '/Applications/ChatGPT.app';
|
|
71
|
+
try { accessSync(bundledCodex(app), constants.X_OK); return app; } catch { return null; }
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export function resolveRealCodex(selfPath) {
|
|
71
75
|
const app = findApp();
|
|
72
|
-
const candidate = setting('REAL_CODEX') || (app &&
|
|
76
|
+
const candidate = setting('REAL_CODEX') || (app && bundledCodex(app));
|
|
73
77
|
if (!candidate || !isAbsolute(candidate)) throw new Error('找不到内置 Codex;可用 MINILINK_REAL_CODEX 指定绝对路径。');
|
|
74
78
|
let actual;
|
|
75
79
|
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,
|
|
8
|
+
import { atomicJSON, configPath, codexHome, readConfig, bundledCodexRelativePath } 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); }
|
|
@@ -26,7 +42,7 @@ function atomicFile(path, text, mode) {
|
|
|
26
42
|
|
|
27
43
|
// Each installation has its own files: active Desktop processes keep their
|
|
28
44
|
// original modules and picker assets while the next launch uses the new copy.
|
|
29
|
-
export function install({ root = installRoot(), plist = defaultPlist(), source = sourceRoot, home = codexHome(), config = configPath(), node = process.execPath, npm = process.env.npm_execpath, prefix = process.env.npm_config_prefix, run = launchctl, legacy = root === installRoot() ? legacyInstallation() : null
|
|
45
|
+
export function install({ root = installRoot(), plist = defaultPlist(), source = sourceRoot, home = codexHome(), config = configPath(), node = process.execPath, npm = process.env.npm_execpath, prefix = process.env.npm_config_prefix, run = launchctl, legacy = root === installRoot() ? legacyInstallation() : null } = {}) {
|
|
30
46
|
if (process.platform !== 'darwin') throw new Error('Desktop 接入目前仅支持 macOS。');
|
|
31
47
|
const previous = installation(root), wrapper = join(root, 'minilink');
|
|
32
48
|
if (existsSync(plist) && !previous) throw new Error('同名 LaunchAgent 已存在且不属于本次安装。');
|
|
@@ -60,17 +76,19 @@ export function install({ root = installRoot(), plist = defaultPlist(), source =
|
|
|
60
76
|
if (JSON.parse(readFileSync(join(dependency, 'package.json'), 'utf8')).name !== 'smol-toml') throw new Error('smol-toml 依赖路径无效。');
|
|
61
77
|
cpSync(dependency, join(runtime, 'node_modules/smol-toml'), { recursive: true });
|
|
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
|
-
//
|
|
64
|
-
|
|
65
|
-
atomicFile(wrapper, `#!/bin/sh\
|
|
79
|
+
// A Desktop process can retain this launcher after npm removes the state.
|
|
80
|
+
const fallback = `if [ ! -f ${shellQuote(stateFile)} ]; then\n exec "\${CODEX_DESKTOP_APP:-/Applications/ChatGPT.app}/${bundledCodexRelativePath}" "$@"\nfi`;
|
|
81
|
+
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
82
|
atomicFile(activate, `#!/bin/sh\nexec ${shellQuote(node)} ${shellQuote(join(runtime, 'src/lifecycle.mjs'))}\n`, 0o700);
|
|
67
83
|
mkdirSync(dirname(plist), { recursive: true });
|
|
68
84
|
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
85
|
atomicJSON(stateFile, state);
|
|
70
86
|
run(['bootout', `gui/${process.getuid()}/${label}`], false);
|
|
71
87
|
run(['bootstrap', `gui/${process.getuid()}`, plist]);
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
let enabled = false;
|
|
89
|
+
try { enabled = readConfig(config).enabled; } catch {}
|
|
90
|
+
syncLaunchEnvironment({ state, run, enabled, previousWrapper: legacy?.state.wrapper });
|
|
91
|
+
if (enabled && run(['getenv', 'CODEX_CLI_PATH'], false) !== wrapper) throw new Error('启动环境验证失败。');
|
|
74
92
|
if (legacy) {
|
|
75
93
|
run(['bootout', `gui/${process.getuid()}/${legacy.label}`], false); retired = true;
|
|
76
94
|
atomicFile(legacy.state.wrapper, `#!/bin/sh\nexec ${shellQuote(wrapper)} "$@"\n`, 0o755);
|
|
@@ -117,8 +135,7 @@ export function reconcile({ root = installRoot(), plist = defaultPlist(), run =
|
|
|
117
135
|
if (existsSync(join(state.source, 'package.json')) || busy(root)) {
|
|
118
136
|
if (!busy(root)) pruneReleases(root, state, now);
|
|
119
137
|
rmSync(marker, { force: true });
|
|
120
|
-
|
|
121
|
-
if (!env) run(['setenv', 'CODEX_CLI_PATH', state.wrapper]);
|
|
138
|
+
syncLaunchEnvironment({ state, run });
|
|
122
139
|
return;
|
|
123
140
|
}
|
|
124
141
|
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',
|
|
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;
|