@offerpilot/axiomruntime 0.0.1 → 0.0.2
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 +17 -10
- package/dist/cli/commands/set.js +121 -0
- package/dist/cli/index.js +11 -1
- package/dist/cli/sanitize-args.js +6 -0
- package/dist/core/integrations/codex-provider-config.js +541 -0
- package/dist/core/runner/claude-permission-policy.js +19 -0
- package/dist/core/runner/engine-registry.js +4 -2
- package/dist/core/runner/tool-runner.js +46 -9
- package/dist/core/status/doctor-service.js +17 -0
- package/dist/telegram/bot-registry.js +17 -4
- package/dist/telegram/engine/claude-engine.js +23 -6
- package/dist/telegram/engine/engine-utils.js +14 -3
- package/docs/USAGE.html +40 -8
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -26,33 +26,34 @@ AI 与维护者使用的英文权威文档位于 `AGENTS.md`、`docs/AI_RUNTIME_
|
|
|
26
26
|
|
|
27
27
|
## 通过 npm 安装
|
|
28
28
|
|
|
29
|
-
运行环境需要 Node.js `22.19.0`
|
|
29
|
+
运行环境需要 Node.js `22.19.0` 或更高版本。先确认版本,再执行全局安装;npm 会把本包注册为 `ai` 命令:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
+
node --version
|
|
32
33
|
npm install --global @offerpilot/axiomruntime
|
|
33
34
|
ai --version
|
|
34
35
|
ai setup
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
如果 npm 显示 `EBADENGINE`,说明当前 Node.js 版本不受支持。请先升级到 `22.19.0+`,不要继续使用警告状态下安装的 CLI。
|
|
39
|
+
|
|
37
40
|
`ai --version` 用于验证 npm 包和命令安装成功;`ai setup` 初始化 Runtime 配置,并可在确认后安装缺失的 Claude Code/Codex。之后可直接运行 `ai`。
|
|
38
41
|
|
|
42
|
+
可用 root 完成全局安装,但日常执行 `ai` 和常驻服务应使用专用非 root 用户。若仍以 root 启动 Claude,Runtime 会把 `auto`/`bypassPermissions` 安全约束为默认权限、输出告警,并且绝不会传递 Claude Code 禁止 root 使用的 `--dangerously-skip-permissions`。
|
|
43
|
+
|
|
39
44
|
## 通过 Homebrew 安装
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
维护者发布新版本时可直接双击仓库根目录的 `发布.command`,交互选择 npm、Homebrew 或发布条件检查,无需记发布命令。npm 发布成功后,再选择 Homebrew;助手会读取 npm Registry 的真实 tarball 与 SHA256,并把 `Formula/axiomruntime.rb` 同步到 GitLab 项目 `linlangli/axiom`。
|
|
47
|
+
|
|
48
|
+
用户首次安装需要显式提供 GitLab 地址:
|
|
42
49
|
|
|
43
50
|
```bash
|
|
44
|
-
brew tap
|
|
45
|
-
brew install axiomruntime
|
|
51
|
+
brew tap linlangli/axiom https://gitlab.com/linlangli/axiom.git
|
|
52
|
+
brew install linlangli/axiom/axiomruntime
|
|
46
53
|
ai --version
|
|
47
54
|
ai setup
|
|
48
55
|
```
|
|
49
56
|
|
|
50
|
-
如果 Tap 不托管在 GitHub,需要在首次添加时显式提供公开 Git 地址:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
brew tap offerpilot/tap <homebrew-tap-public-git-url>
|
|
54
|
-
```
|
|
55
|
-
|
|
56
57
|
## 安装开发依赖
|
|
57
58
|
|
|
58
59
|
运行和开发需要 Node.js `22.19.0` 或更高版本。该下限与锁定依赖的运行时要求一致;Node.js 20 不受支持。
|
|
@@ -103,6 +104,8 @@ AI_GATEWAY_SERVER_TOKEN='replace-me' npm run server
|
|
|
103
104
|
|
|
104
105
|
默认分支提交通过测试后,GitLab CI 会构建以 commit SHA 标记的镜像,并由锁定当前项目的受保护 Runner(标签 `axiomruntime-prod`)部署到 `154.201.73.41:/opt/AxiomRuntime`。部署只操作 `axiomruntime` Compose 项目;健康检查失败时恢复上一镜像,不清理服务器上的其他容器或镜像。
|
|
105
106
|
|
|
107
|
+
镜像构建会在一次性 build stage 中编译 `better-sqlite3` 原生 addon,再把裁掉开发依赖后的 production dependency tree 复制到相同 Node.js 版本的最终镜像;最终镜像不包含编译器。
|
|
108
|
+
|
|
106
109
|
首次部署前,服务器必须已有 `/opt/AxiomRuntime/config/providers.json` 和 `/opt/AxiomRuntime/secrets/server-token`,且容器用户 `10001:10001` 可以读写配置目录。CI 只校验这些文件,不生成或覆盖凭据。
|
|
107
110
|
|
|
108
111
|
Telegram 容器默认不启动,以免与现有 long polling 进程争抢消息。完成迁移并停止旧 Bot 后,在服务器 `/opt/AxiomRuntime/.env` 中设置:
|
|
@@ -130,6 +133,10 @@ ai provider add
|
|
|
130
133
|
ai provider list
|
|
131
134
|
ai provider edit
|
|
132
135
|
ai provider delete
|
|
136
|
+
ai set codex chatgpt
|
|
137
|
+
ai set codex provider 51talk-cq gpt-5.6-sol
|
|
138
|
+
ai set codex provider 51talk-cq
|
|
139
|
+
ai set codex proxy 127.0.0.1:7890
|
|
133
140
|
ai telegram
|
|
134
141
|
ai telegram list
|
|
135
142
|
ai telegram start --bot ops
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { writeLog } from "../../core/logs/log-service.js";
|
|
2
|
+
import { readCache } from "../../core/config/cache-store.js";
|
|
3
|
+
import { listAvailableCodexGptModels, resolveCodexProviderModel, setCodexProvider, setCodexProxy } from "../../core/integrations/codex-provider-config.js";
|
|
4
|
+
import { listProviders } from "../../core/providers/provider-service.js";
|
|
5
|
+
import { promptSelect, promptText } from "../prompts/prompt.js";
|
|
6
|
+
const SET_CODEX_USAGE = "Usage: ai set codex [chatgpt | provider [name] [model-id] | proxy <host:port|url|off>]";
|
|
7
|
+
export async function runSetCommand(args) {
|
|
8
|
+
const [targetArg, modeArg, providerNameArg, modelArg, ...extra] = args;
|
|
9
|
+
const target = targetArg ?? "codex";
|
|
10
|
+
if (target !== "codex") {
|
|
11
|
+
throw new Error(SET_CODEX_USAGE);
|
|
12
|
+
}
|
|
13
|
+
if (extra.length) {
|
|
14
|
+
throw new Error(SET_CODEX_USAGE);
|
|
15
|
+
}
|
|
16
|
+
const mode = await getMode(modeArg);
|
|
17
|
+
if (mode === "proxy") {
|
|
18
|
+
if (modelArg)
|
|
19
|
+
throw new Error(SET_CODEX_USAGE);
|
|
20
|
+
const proxyValue = providerNameArg ?? await promptText("Codex proxy (host:port, URL, or off)");
|
|
21
|
+
const result = await setCodexProxy(proxyValue);
|
|
22
|
+
console.log(result.proxyUrl ? `Codex proxy set to: ${result.proxyUrl}` : "Codex proxy cleared.");
|
|
23
|
+
console.log(`Updated: ${result.configPath}`);
|
|
24
|
+
if (!result.environment.applied && result.environment.reason) {
|
|
25
|
+
console.log(result.environment.reason);
|
|
26
|
+
}
|
|
27
|
+
printRestartResult(result.restart);
|
|
28
|
+
await writeLog({
|
|
29
|
+
category: "provider",
|
|
30
|
+
action: "codex_proxy_set",
|
|
31
|
+
message: result.proxyUrl ? "Codex proxy enabled." : "Codex proxy cleared.",
|
|
32
|
+
metadata: {
|
|
33
|
+
enabled: Boolean(result.proxyUrl),
|
|
34
|
+
desktopEnvironmentApplied: result.environment.applied,
|
|
35
|
+
restartScheduled: result.restart.scheduled
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (mode === "chatgpt" && (providerNameArg || modelArg)) {
|
|
41
|
+
throw new Error("Usage: ai set codex chatgpt");
|
|
42
|
+
}
|
|
43
|
+
const selection = mode === "chatgpt"
|
|
44
|
+
? { type: "chatgpt" }
|
|
45
|
+
: { type: "provider", ...await getProviderAndModel(providerNameArg, modelArg) };
|
|
46
|
+
if (mode === "chatgpt") {
|
|
47
|
+
console.log("Opening ChatGPT login in your browser...");
|
|
48
|
+
}
|
|
49
|
+
const result = await setCodexProvider(selection);
|
|
50
|
+
console.log(`Codex provider set to: ${result.providerName}`);
|
|
51
|
+
if (result.model)
|
|
52
|
+
console.log(`Codex model set to: ${result.model}`);
|
|
53
|
+
console.log(`Updated: ${result.configPath}`);
|
|
54
|
+
console.log(`Updated: ${result.authPath}`);
|
|
55
|
+
printRestartResult(result.restart);
|
|
56
|
+
await writeLog({
|
|
57
|
+
category: "provider",
|
|
58
|
+
action: "codex_provider_set",
|
|
59
|
+
message: `Codex provider set to ${result.providerName}.`,
|
|
60
|
+
metadata: {
|
|
61
|
+
selection: result.selection,
|
|
62
|
+
provider: result.providerName,
|
|
63
|
+
providerId: result.providerId,
|
|
64
|
+
model: result.model,
|
|
65
|
+
restartScheduled: result.restart.scheduled
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function getMode(value) {
|
|
70
|
+
if (value === "chatgpt" || value === "provider" || value === "proxy")
|
|
71
|
+
return value;
|
|
72
|
+
if (value) {
|
|
73
|
+
throw new Error(`Unknown Codex setting: ${value}`);
|
|
74
|
+
}
|
|
75
|
+
return promptSelect("Set Codex", [
|
|
76
|
+
{ label: "ChatGPT account", value: "chatgpt" },
|
|
77
|
+
{ label: "Configured provider", value: "provider" },
|
|
78
|
+
{ label: "Proxy", value: "proxy" }
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
function printRestartResult(restart) {
|
|
82
|
+
if (restart.scheduled) {
|
|
83
|
+
console.log(`Restarting ${restart.appPath ?? "Codex app"}...`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.log(`Restart Codex app manually. ${restart.reason ?? ""}`.trim());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function getProviderAndModel(name, requestedModel) {
|
|
90
|
+
const [providers, cache] = await Promise.all([listProviders(), readCache()]);
|
|
91
|
+
return selectCodexProviderAndModel(providers, cache.providers, name, requestedModel);
|
|
92
|
+
}
|
|
93
|
+
export async function selectCodexProviderAndModel(providers, providerCache, name, requestedModel, select = promptSelect) {
|
|
94
|
+
if (!providers.length) {
|
|
95
|
+
throw new Error("No providers configured. Run `ai provider add` first.");
|
|
96
|
+
}
|
|
97
|
+
if (!name && !providers.some((provider) => providerGptModels(provider.name, providerCache).length)) {
|
|
98
|
+
throw new Error("No configured provider has available GPT models. Run `ai status` to refresh model lists.");
|
|
99
|
+
}
|
|
100
|
+
const providerName = name ?? await select("Select provider for Codex", providers.map((provider) => ({
|
|
101
|
+
label: `${provider.name} / ${providerGptModels(provider.name, providerCache).length} GPT models`,
|
|
102
|
+
value: provider.name
|
|
103
|
+
})));
|
|
104
|
+
const provider = providers.find((item) => item.name === providerName);
|
|
105
|
+
if (!provider) {
|
|
106
|
+
throw new Error(`Provider not found: ${providerName}`);
|
|
107
|
+
}
|
|
108
|
+
const availableModels = providerGptModels(provider.name, providerCache);
|
|
109
|
+
if (!availableModels.length) {
|
|
110
|
+
resolveCodexProviderModel(provider.name, availableModels);
|
|
111
|
+
}
|
|
112
|
+
const model = requestedModel
|
|
113
|
+
? resolveCodexProviderModel(provider.name, availableModels, requestedModel)
|
|
114
|
+
: name
|
|
115
|
+
? resolveCodexProviderModel(provider.name, availableModels)
|
|
116
|
+
: await select(`Select GPT model for ${provider.name}`, availableModels.map((availableModel) => ({ label: availableModel, value: availableModel })));
|
|
117
|
+
return { provider, model };
|
|
118
|
+
}
|
|
119
|
+
function providerGptModels(providerName, providerCache) {
|
|
120
|
+
return listAvailableCodexGptModels(providerCache[providerName]?.models ?? []);
|
|
121
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { runMemoryCommand } from "./commands/memory.js";
|
|
|
10
10
|
import { runReportCommand } from "./commands/report.js";
|
|
11
11
|
import { runLastCommand, runUseCommand } from "./commands/use.js";
|
|
12
12
|
import { runSessionCommand } from "./commands/session.js";
|
|
13
|
+
import { runSetCommand } from "./commands/set.js";
|
|
13
14
|
import { runSetupCommand } from "./commands/setup.js";
|
|
14
15
|
import { runStatusCommand } from "./commands/status.js";
|
|
15
16
|
import { runTelegramCommand } from "./commands/telegram.js";
|
|
@@ -17,6 +18,7 @@ import { runVersionCommand } from "./commands/version.js";
|
|
|
17
18
|
import { writeLog } from "../core/logs/log-service.js";
|
|
18
19
|
import { promptSelect } from "./prompts/prompt.js";
|
|
19
20
|
import { getCliCommand, registerCliCommand } from "./registry.js";
|
|
21
|
+
import { redactCodexProxyCommandArgs } from "./sanitize-args.js";
|
|
20
22
|
registerBuiltInCommands();
|
|
21
23
|
async function main() {
|
|
22
24
|
const [, , command, ...args] = process.argv;
|
|
@@ -52,6 +54,9 @@ async function main() {
|
|
|
52
54
|
process.exitCode = 1;
|
|
53
55
|
}
|
|
54
56
|
function sanitizeArgs(command, args) {
|
|
57
|
+
const redactedProxyArgs = redactCodexProxyCommandArgs(command, args);
|
|
58
|
+
if (redactedProxyArgs)
|
|
59
|
+
return redactedProxyArgs;
|
|
55
60
|
if (command === "add" && args.length >= 3) {
|
|
56
61
|
return [args[0], args[1], maskSecretArg(args[2])];
|
|
57
62
|
}
|
|
@@ -131,7 +136,7 @@ function maskSecretArg(value) {
|
|
|
131
136
|
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
|
132
137
|
}
|
|
133
138
|
async function runInteractive() {
|
|
134
|
-
const interactiveCommandNames = ["setup", "use", "add", "memory", "telegram", "status", "session", "report", "doctor", "log", "context", "help", "last"];
|
|
139
|
+
const interactiveCommandNames = ["setup", "use", "set", "add", "memory", "telegram", "status", "session", "report", "doctor", "log", "context", "help", "last"];
|
|
135
140
|
const command = await promptSelect("AI Gateway", [
|
|
136
141
|
...interactiveCommandNames
|
|
137
142
|
.flatMap((name) => {
|
|
@@ -250,6 +255,11 @@ function registerBuiltInCommands() {
|
|
|
250
255
|
description: "use",
|
|
251
256
|
run: (args) => runUseCommand(args)
|
|
252
257
|
});
|
|
258
|
+
registerCliCommand({
|
|
259
|
+
name: "set",
|
|
260
|
+
description: "set Codex provider or proxy",
|
|
261
|
+
run: (args) => runSetCommand(args)
|
|
262
|
+
});
|
|
253
263
|
registerCliCommand({
|
|
254
264
|
name: "last",
|
|
255
265
|
description: "last",
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import lockfile from "proper-lockfile";
|
|
7
|
+
import { resolveToolCommand } from "../runner/command-resolver.js";
|
|
8
|
+
const LOCK_OPTIONS = {
|
|
9
|
+
realpath: false,
|
|
10
|
+
stale: 10_000,
|
|
11
|
+
update: 5_000,
|
|
12
|
+
retries: {
|
|
13
|
+
retries: 100,
|
|
14
|
+
factor: 1.2,
|
|
15
|
+
minTimeout: 25,
|
|
16
|
+
maxTimeout: 500
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const RESERVED_PROVIDER_IDS = new Set(["openai", "ollama", "lmstudio"]);
|
|
20
|
+
const CODEX_PROXY_ENV_KEYS = [
|
|
21
|
+
"HTTP_PROXY",
|
|
22
|
+
"HTTPS_PROXY",
|
|
23
|
+
"ALL_PROXY",
|
|
24
|
+
"http_proxy",
|
|
25
|
+
"https_proxy",
|
|
26
|
+
"all_proxy"
|
|
27
|
+
];
|
|
28
|
+
export async function setCodexProvider(selection, options = {}) {
|
|
29
|
+
const codexHome = resolveCodexHome(options.codexHome);
|
|
30
|
+
const configPath = path.join(codexHome, "config.toml");
|
|
31
|
+
const authPath = path.join(codexHome, "auth.json");
|
|
32
|
+
await fs.mkdir(codexHome, { recursive: true, mode: 0o700 });
|
|
33
|
+
const release = await lockfile.lock(configPath, LOCK_OPTIONS);
|
|
34
|
+
let providerName;
|
|
35
|
+
let providerId;
|
|
36
|
+
let model;
|
|
37
|
+
try {
|
|
38
|
+
if (selection.type === "chatgpt") {
|
|
39
|
+
providerName = "ChatGPT";
|
|
40
|
+
providerId = "openai";
|
|
41
|
+
model = null;
|
|
42
|
+
await configureChatGpt(configPath, authPath, codexHome, options.runChatGptLogin);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
providerName = selection.provider.name;
|
|
46
|
+
providerId = providerIdForName(selection.provider.name);
|
|
47
|
+
model = assertCodexGptModel(selection.model);
|
|
48
|
+
await configureCustomProvider(configPath, authPath, selection.provider, providerId, model);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await release();
|
|
53
|
+
}
|
|
54
|
+
const restart = await (options.scheduleRestart ?? scheduleCodexAppRestart)();
|
|
55
|
+
return {
|
|
56
|
+
selection: selection.type,
|
|
57
|
+
providerName,
|
|
58
|
+
providerId,
|
|
59
|
+
model,
|
|
60
|
+
configPath,
|
|
61
|
+
authPath,
|
|
62
|
+
restart
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export async function setCodexProxy(value, options = {}) {
|
|
66
|
+
const proxyUrl = normalizeCodexProxy(value);
|
|
67
|
+
const codexHome = resolveCodexHome(options.codexHome);
|
|
68
|
+
const configPath = path.join(codexHome, "config.toml");
|
|
69
|
+
await fs.mkdir(codexHome, { recursive: true, mode: 0o700 });
|
|
70
|
+
const release = await lockfile.lock(configPath, LOCK_OPTIONS);
|
|
71
|
+
let environment;
|
|
72
|
+
try {
|
|
73
|
+
const snapshot = await snapshotFile(configPath);
|
|
74
|
+
const current = snapshot?.content.toString("utf8") ?? "";
|
|
75
|
+
try {
|
|
76
|
+
if (snapshot || proxyUrl !== null) {
|
|
77
|
+
await atomicWriteFile(configPath, buildCodexProxyConfig(current, proxyUrl), snapshot?.mode ?? 0o600);
|
|
78
|
+
}
|
|
79
|
+
environment = await (options.applyProxyEnvironment
|
|
80
|
+
?? ((nextProxyUrl) => applyCodexProxyEnvironment(nextProxyUrl, options.platform)))(proxyUrl);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
await restoreFile(configPath, snapshot).catch(() => undefined);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await release();
|
|
89
|
+
}
|
|
90
|
+
const restart = await (options.scheduleRestart ?? scheduleCodexAppRestart)();
|
|
91
|
+
return { proxyUrl, configPath, environment, restart };
|
|
92
|
+
}
|
|
93
|
+
export function providerIdForName(name) {
|
|
94
|
+
const trimmed = name.trim();
|
|
95
|
+
if (/^[A-Za-z0-9_-]+$/.test(trimmed) && !RESERVED_PROVIDER_IDS.has(trimmed.toLowerCase())) {
|
|
96
|
+
return trimmed;
|
|
97
|
+
}
|
|
98
|
+
const slug = trimmed
|
|
99
|
+
.normalize("NFKD")
|
|
100
|
+
.replace(/[^A-Za-z0-9_-]+/g, "-")
|
|
101
|
+
.replace(/^-+|-+$/g, "")
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.slice(0, 40) || "provider";
|
|
104
|
+
const digest = createHash("sha256").update(trimmed).digest("hex").slice(0, 8);
|
|
105
|
+
return `ai-${slug}-${digest}`;
|
|
106
|
+
}
|
|
107
|
+
export function listAvailableCodexGptModels(models) {
|
|
108
|
+
return [...new Set(models.map((model) => model.trim()).filter(isCodexGptModel))]
|
|
109
|
+
.sort(compareCodexGptModelsNewestFirst);
|
|
110
|
+
}
|
|
111
|
+
export function resolveCodexProviderModel(providerName, models, requestedModel) {
|
|
112
|
+
const available = listAvailableCodexGptModels(models);
|
|
113
|
+
if (!available.length) {
|
|
114
|
+
throw new Error(`Provider ${providerName} has no available GPT models. Run \`ai status\` to refresh its model list.`);
|
|
115
|
+
}
|
|
116
|
+
if (!requestedModel)
|
|
117
|
+
return available[0];
|
|
118
|
+
const requested = assertCodexGptModel(requestedModel);
|
|
119
|
+
const selected = available.find((model) => model.toLowerCase() === requested.toLowerCase());
|
|
120
|
+
if (!selected) {
|
|
121
|
+
throw new Error(`GPT model ${requested} is not available for provider ${providerName}. Available: ${available.join(", ")}`);
|
|
122
|
+
}
|
|
123
|
+
return selected;
|
|
124
|
+
}
|
|
125
|
+
export function buildCodexProviderConfig(current, provider, providerId, model = provider.model) {
|
|
126
|
+
if (!provider.baseUrl.trim())
|
|
127
|
+
throw new Error(`Provider ${provider.name} has no base URL.`);
|
|
128
|
+
if (!provider.apiKey.trim())
|
|
129
|
+
throw new Error(`Provider ${provider.name} has no API key.`);
|
|
130
|
+
const selectedModel = assertCodexGptModel(model);
|
|
131
|
+
const eol = current.includes("\r\n") ? "\r\n" : "\n";
|
|
132
|
+
let next = removeProviderTables(current, providerId);
|
|
133
|
+
next = setTopLevelTomlString(next, "cli_auth_credentials_store", "file");
|
|
134
|
+
next = setTopLevelTomlString(next, "model_provider", providerId);
|
|
135
|
+
next = setTopLevelTomlString(next, "model", selectedModel);
|
|
136
|
+
const table = [
|
|
137
|
+
`[model_providers.${providerId}]`,
|
|
138
|
+
`name = ${tomlString(provider.name)}`,
|
|
139
|
+
`base_url = ${tomlString(provider.baseUrl.trim().replace(/\/+$/, ""))}`,
|
|
140
|
+
`env_key = ${tomlString("OPENAI_API_KEY")}`,
|
|
141
|
+
"requires_openai_auth = true",
|
|
142
|
+
`wire_api = ${tomlString("responses")}`
|
|
143
|
+
].join(eol);
|
|
144
|
+
return `${next.trimEnd()}${eol}${eol}${table}${eol}`;
|
|
145
|
+
}
|
|
146
|
+
export function buildCodexChatGptConfig(current) {
|
|
147
|
+
let next = setTopLevelTomlString(current, "cli_auth_credentials_store", "file");
|
|
148
|
+
next = setTopLevelTomlString(next, "model_provider", "openai");
|
|
149
|
+
return ensureTrailingNewline(next);
|
|
150
|
+
}
|
|
151
|
+
export function normalizeCodexProxy(value) {
|
|
152
|
+
const trimmed = value.trim();
|
|
153
|
+
if (!trimmed) {
|
|
154
|
+
throw new Error("Proxy address is required. Example: 127.0.0.1:7890");
|
|
155
|
+
}
|
|
156
|
+
if (["off", "none", "clear", "disable"].includes(trimmed.toLowerCase()))
|
|
157
|
+
return null;
|
|
158
|
+
const candidate = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(trimmed)
|
|
159
|
+
? trimmed
|
|
160
|
+
: `http://${trimmed}`;
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = new URL(candidate);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new Error(`Invalid proxy address: ${value}`);
|
|
167
|
+
}
|
|
168
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
169
|
+
throw new Error("Codex proxy must use http:// or https://.");
|
|
170
|
+
}
|
|
171
|
+
if (!parsed.hostname)
|
|
172
|
+
throw new Error(`Invalid proxy address: ${value}`);
|
|
173
|
+
if (parsed.username || parsed.password) {
|
|
174
|
+
throw new Error("Proxy credentials are not accepted on the command line; use a local unauthenticated proxy endpoint.");
|
|
175
|
+
}
|
|
176
|
+
if ((parsed.pathname && parsed.pathname !== "/") || parsed.search || parsed.hash) {
|
|
177
|
+
throw new Error("Codex proxy must be an origin only, without a path, query, or fragment.");
|
|
178
|
+
}
|
|
179
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
180
|
+
}
|
|
181
|
+
export function buildCodexProxyConfig(current, proxyUrl) {
|
|
182
|
+
return updateTomlTableStrings(current, "shell_environment_policy.set", Object.fromEntries(CODEX_PROXY_ENV_KEYS.map((key) => [key, proxyUrl])));
|
|
183
|
+
}
|
|
184
|
+
export async function applyCodexProxyEnvironment(proxyUrl, platform = process.platform) {
|
|
185
|
+
if (platform !== "darwin") {
|
|
186
|
+
return {
|
|
187
|
+
applied: false,
|
|
188
|
+
reason: "Automatic desktop proxy environment updates are currently supported on macOS only."
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const snapshots = new Map();
|
|
192
|
+
for (const key of CODEX_PROXY_ENV_KEYS) {
|
|
193
|
+
const result = await runProcess("/bin/launchctl", ["getenv", key]);
|
|
194
|
+
const previous = result.code === 0 ? result.stdout.replace(/\r?\n$/, "") : "";
|
|
195
|
+
snapshots.set(key, previous || null);
|
|
196
|
+
}
|
|
197
|
+
const changed = [];
|
|
198
|
+
try {
|
|
199
|
+
for (const key of CODEX_PROXY_ENV_KEYS) {
|
|
200
|
+
const args = proxyUrl === null ? ["unsetenv", key] : ["setenv", key, proxyUrl];
|
|
201
|
+
const result = await runProcess("/bin/launchctl", args);
|
|
202
|
+
if (result.code !== 0) {
|
|
203
|
+
throw new Error(`launchctl ${args[0]} failed for ${key}: ${result.stderr.trim() || `exit ${result.code}`}`);
|
|
204
|
+
}
|
|
205
|
+
changed.push(key);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
for (const key of changed.reverse()) {
|
|
210
|
+
const previous = snapshots.get(key);
|
|
211
|
+
const args = previous === null || previous === undefined
|
|
212
|
+
? ["unsetenv", key]
|
|
213
|
+
: ["setenv", key, previous];
|
|
214
|
+
await runProcess("/bin/launchctl", args).catch(() => undefined);
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
return { applied: true };
|
|
219
|
+
}
|
|
220
|
+
export async function scheduleCodexAppRestart(options = {}) {
|
|
221
|
+
const platform = options.platform ?? process.platform;
|
|
222
|
+
if (platform !== "darwin") {
|
|
223
|
+
return { scheduled: false, reason: "Automatic Codex app restart is currently supported on macOS only." };
|
|
224
|
+
}
|
|
225
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
226
|
+
const candidates = options.appCandidates ?? [
|
|
227
|
+
"/Applications/Codex.app",
|
|
228
|
+
"/Applications/ChatGPT.app",
|
|
229
|
+
path.join(homeDir, "Applications/Codex.app"),
|
|
230
|
+
path.join(homeDir, "Applications/ChatGPT.app")
|
|
231
|
+
];
|
|
232
|
+
const pathExists = options.pathExists ?? fileExists;
|
|
233
|
+
let appPath;
|
|
234
|
+
for (const candidate of candidates) {
|
|
235
|
+
if (await pathExists(candidate)) {
|
|
236
|
+
appPath = candidate;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!appPath) {
|
|
241
|
+
return { scheduled: false, reason: "Codex.app or ChatGPT.app was not found." };
|
|
242
|
+
}
|
|
243
|
+
const appName = path.basename(appPath, ".app");
|
|
244
|
+
if (!/^[A-Za-z0-9 ._-]+$/.test(appName)) {
|
|
245
|
+
return { scheduled: false, reason: `Unsupported application name: ${appName}` };
|
|
246
|
+
}
|
|
247
|
+
const appleScriptName = appName.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
248
|
+
const helper = [
|
|
249
|
+
"const { spawnSync } = require('node:child_process');",
|
|
250
|
+
`const appPath = ${JSON.stringify(appPath)};`,
|
|
251
|
+
`const quitScript = ${JSON.stringify(`tell application "${appleScriptName}" to quit`)};`,
|
|
252
|
+
"setTimeout(() => {",
|
|
253
|
+
" spawnSync('/usr/bin/osascript', ['-e', quitScript], { stdio: 'ignore' });",
|
|
254
|
+
" setTimeout(() => spawnSync('/usr/bin/open', [appPath], { stdio: 'ignore' }), 1000);",
|
|
255
|
+
"}, 750);"
|
|
256
|
+
].join("\n");
|
|
257
|
+
(options.spawnDetached ?? defaultSpawnDetached)(process.execPath, ["-e", helper]);
|
|
258
|
+
return { scheduled: true, appPath };
|
|
259
|
+
}
|
|
260
|
+
async function configureChatGpt(configPath, authPath, codexHome, runner) {
|
|
261
|
+
const [configSnapshot, authSnapshot] = await Promise.all([
|
|
262
|
+
snapshotFile(configPath),
|
|
263
|
+
snapshotFile(authPath)
|
|
264
|
+
]);
|
|
265
|
+
try {
|
|
266
|
+
const exitCode = await (runner ?? runCodexChatGptLogin)({ codexHome });
|
|
267
|
+
if (exitCode !== 0) {
|
|
268
|
+
throw new Error(`ChatGPT login failed with exit code ${exitCode}.`);
|
|
269
|
+
}
|
|
270
|
+
await validateChatGptAuth(authPath);
|
|
271
|
+
await fs.chmod(authPath, 0o600);
|
|
272
|
+
const currentConfig = configSnapshot?.content.toString("utf8") ?? "";
|
|
273
|
+
await atomicWriteFile(configPath, buildCodexChatGptConfig(currentConfig), configSnapshot?.mode ?? 0o600);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
await Promise.allSettled([
|
|
277
|
+
restoreFile(configPath, configSnapshot),
|
|
278
|
+
restoreFile(authPath, authSnapshot)
|
|
279
|
+
]);
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function configureCustomProvider(configPath, authPath, provider, providerId, model) {
|
|
284
|
+
const [configSnapshot, authSnapshot] = await Promise.all([
|
|
285
|
+
snapshotFile(configPath),
|
|
286
|
+
snapshotFile(authPath)
|
|
287
|
+
]);
|
|
288
|
+
const currentConfig = configSnapshot?.content.toString("utf8") ?? "";
|
|
289
|
+
const auth = `${JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: provider.apiKey.trim() }, null, 2)}\n`;
|
|
290
|
+
try {
|
|
291
|
+
await atomicWriteFile(authPath, auth, 0o600);
|
|
292
|
+
await atomicWriteFile(configPath, buildCodexProviderConfig(currentConfig, provider, providerId, model), configSnapshot?.mode ?? 0o600);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
await Promise.allSettled([
|
|
296
|
+
restoreFile(configPath, configSnapshot),
|
|
297
|
+
restoreFile(authPath, authSnapshot)
|
|
298
|
+
]);
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function runCodexChatGptLogin({ codexHome }) {
|
|
303
|
+
const command = resolveToolCommand("codex");
|
|
304
|
+
if (!command) {
|
|
305
|
+
throw new Error("Codex CLI not found. Run `ai setup --install-tools` first.");
|
|
306
|
+
}
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const child = spawn(command, ["-c", "cli_auth_credentials_store=\"file\"", "login"], {
|
|
309
|
+
env: { ...process.env, CODEX_HOME: codexHome },
|
|
310
|
+
stdio: "inherit"
|
|
311
|
+
});
|
|
312
|
+
child.once("error", reject);
|
|
313
|
+
child.once("close", (code, signal) => {
|
|
314
|
+
if (signal) {
|
|
315
|
+
reject(new Error(`ChatGPT login was interrupted by ${signal}.`));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
resolve(code ?? 1);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async function validateChatGptAuth(authPath) {
|
|
323
|
+
let auth;
|
|
324
|
+
try {
|
|
325
|
+
auth = JSON.parse(await fs.readFile(authPath, "utf8"));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
throw new Error("ChatGPT login completed without a readable Codex auth.json.");
|
|
329
|
+
}
|
|
330
|
+
if (!isRecord(auth) || auth.auth_mode !== "chatgpt" || !isRecord(auth.tokens)) {
|
|
331
|
+
throw new Error("ChatGPT login did not produce valid Codex credentials.");
|
|
332
|
+
}
|
|
333
|
+
if (typeof auth.tokens.access_token !== "string" || typeof auth.tokens.refresh_token !== "string") {
|
|
334
|
+
throw new Error("ChatGPT login credentials are incomplete.");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function setTopLevelTomlString(current, key, value) {
|
|
338
|
+
const eol = current.includes("\r\n") ? "\r\n" : "\n";
|
|
339
|
+
const lines = current.split(/\r?\n/);
|
|
340
|
+
const firstTable = lines.findIndex((line) => /^\s*\[\[?/.test(line));
|
|
341
|
+
const end = firstTable < 0 ? lines.length : firstTable;
|
|
342
|
+
const assignment = `${key} = ${tomlString(value)}`;
|
|
343
|
+
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
|
|
344
|
+
let replaced = false;
|
|
345
|
+
const top = lines.slice(0, end).filter((line) => {
|
|
346
|
+
if (!pattern.test(line))
|
|
347
|
+
return true;
|
|
348
|
+
if (replaced)
|
|
349
|
+
return false;
|
|
350
|
+
replaced = true;
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
if (replaced) {
|
|
354
|
+
const index = top.findIndex((line) => pattern.test(line));
|
|
355
|
+
top[index] = assignment;
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
while (top.length && top[top.length - 1] === "")
|
|
359
|
+
top.pop();
|
|
360
|
+
top.push(assignment, "");
|
|
361
|
+
}
|
|
362
|
+
return [...top, ...lines.slice(end)].join(eol);
|
|
363
|
+
}
|
|
364
|
+
function updateTomlTableStrings(current, tableName, values) {
|
|
365
|
+
const eol = current.includes("\r\n") ? "\r\n" : "\n";
|
|
366
|
+
const lines = current.split(/\r?\n/);
|
|
367
|
+
const tablePattern = new RegExp(`^\\s*\\[${escapeRegExp(tableName)}\\]\\s*(?:#.*)?$`);
|
|
368
|
+
const tableIndex = lines.findIndex((line) => tablePattern.test(line));
|
|
369
|
+
const assignments = Object.entries(values)
|
|
370
|
+
.filter((entry) => entry[1] !== null)
|
|
371
|
+
.map(([key, value]) => `${key} = ${tomlString(value)}`);
|
|
372
|
+
if (tableIndex < 0) {
|
|
373
|
+
if (!assignments.length)
|
|
374
|
+
return ensureTrailingNewline(current);
|
|
375
|
+
const prefix = current.trimEnd();
|
|
376
|
+
const table = [`[${tableName}]`, ...assignments].join(eol);
|
|
377
|
+
return `${prefix ? `${prefix}${eol}${eol}` : ""}${table}${eol}`;
|
|
378
|
+
}
|
|
379
|
+
let tableEnd = lines.length;
|
|
380
|
+
for (let index = tableIndex + 1; index < lines.length; index += 1) {
|
|
381
|
+
if (/^\s*\[\[?/.test(lines[index])) {
|
|
382
|
+
tableEnd = index;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const managedPatterns = Object.keys(values).map((key) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`));
|
|
387
|
+
const body = lines
|
|
388
|
+
.slice(tableIndex + 1, tableEnd)
|
|
389
|
+
.filter((line) => !managedPatterns.some((pattern) => pattern.test(line)));
|
|
390
|
+
while (body.length && body[body.length - 1] === "")
|
|
391
|
+
body.pop();
|
|
392
|
+
if (body.length && assignments.length)
|
|
393
|
+
body.push("");
|
|
394
|
+
body.push(...assignments);
|
|
395
|
+
return ensureTrailingNewline([
|
|
396
|
+
...lines.slice(0, tableIndex + 1),
|
|
397
|
+
...body,
|
|
398
|
+
...lines.slice(tableEnd)
|
|
399
|
+
].join(eol));
|
|
400
|
+
}
|
|
401
|
+
function removeProviderTables(current, providerId) {
|
|
402
|
+
const lines = current.split(/\r?\n/);
|
|
403
|
+
const prefix = `model_providers.${providerId}`;
|
|
404
|
+
let skip = false;
|
|
405
|
+
const kept = [];
|
|
406
|
+
for (const line of lines) {
|
|
407
|
+
const header = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line)?.[1];
|
|
408
|
+
if (header) {
|
|
409
|
+
skip = header === prefix || header.startsWith(`${prefix}.`);
|
|
410
|
+
}
|
|
411
|
+
if (!skip)
|
|
412
|
+
kept.push(line);
|
|
413
|
+
}
|
|
414
|
+
return kept.join(current.includes("\r\n") ? "\r\n" : "\n");
|
|
415
|
+
}
|
|
416
|
+
async function snapshotFile(filePath) {
|
|
417
|
+
try {
|
|
418
|
+
const [content, stat] = await Promise.all([fs.readFile(filePath), fs.stat(filePath)]);
|
|
419
|
+
return { content, mode: stat.mode & 0o777 };
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (error.code === "ENOENT")
|
|
423
|
+
return null;
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function restoreFile(filePath, snapshot) {
|
|
428
|
+
if (!snapshot) {
|
|
429
|
+
await fs.rm(filePath, { force: true });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
await atomicWriteFile(filePath, snapshot.content, snapshot.mode);
|
|
433
|
+
}
|
|
434
|
+
async function atomicWriteFile(filePath, content, mode) {
|
|
435
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
436
|
+
const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
437
|
+
try {
|
|
438
|
+
await fs.writeFile(temporaryPath, content, { mode });
|
|
439
|
+
await fs.chmod(temporaryPath, mode);
|
|
440
|
+
await fs.rename(temporaryPath, filePath);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function resolveCodexHome(explicit) {
|
|
448
|
+
const value = explicit ?? process.env.CODEX_HOME;
|
|
449
|
+
return value ? path.resolve(value) : path.join(os.homedir(), ".codex");
|
|
450
|
+
}
|
|
451
|
+
function ensureTrailingNewline(value) {
|
|
452
|
+
return `${value.trimEnd()}\n`;
|
|
453
|
+
}
|
|
454
|
+
function tomlString(value) {
|
|
455
|
+
return JSON.stringify(value);
|
|
456
|
+
}
|
|
457
|
+
function escapeRegExp(value) {
|
|
458
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
459
|
+
}
|
|
460
|
+
function assertCodexGptModel(model) {
|
|
461
|
+
const value = model.trim();
|
|
462
|
+
if (!isCodexGptModel(value)) {
|
|
463
|
+
throw new Error(`Codex provider model must be a GPT model id starting with \"gpt-\": ${model || "(empty)"}`);
|
|
464
|
+
}
|
|
465
|
+
return value;
|
|
466
|
+
}
|
|
467
|
+
function isCodexGptModel(model) {
|
|
468
|
+
return /^gpt-/i.test(model.trim());
|
|
469
|
+
}
|
|
470
|
+
function compareCodexGptModelsNewestFirst(left, right) {
|
|
471
|
+
const leftKey = codexGptModelSortKey(left);
|
|
472
|
+
const rightKey = codexGptModelSortKey(right);
|
|
473
|
+
const versionLength = Math.max(leftKey.version.length, rightKey.version.length);
|
|
474
|
+
for (let index = 0; index < versionLength; index += 1) {
|
|
475
|
+
const difference = (rightKey.version[index] ?? -1) - (leftKey.version[index] ?? -1);
|
|
476
|
+
if (difference)
|
|
477
|
+
return difference;
|
|
478
|
+
}
|
|
479
|
+
if (leftKey.tier !== rightKey.tier)
|
|
480
|
+
return rightKey.tier - leftKey.tier;
|
|
481
|
+
if (leftKey.alias !== rightKey.alias)
|
|
482
|
+
return rightKey.alias - leftKey.alias;
|
|
483
|
+
if (leftKey.snapshot !== rightKey.snapshot)
|
|
484
|
+
return rightKey.snapshot - leftKey.snapshot;
|
|
485
|
+
return left.localeCompare(right);
|
|
486
|
+
}
|
|
487
|
+
function codexGptModelSortKey(model) {
|
|
488
|
+
const value = model.toLowerCase();
|
|
489
|
+
const version = /^gpt-(\d+(?:\.\d+)*)/.exec(value)?.[1]
|
|
490
|
+
?.split(".")
|
|
491
|
+
.map(Number) ?? [];
|
|
492
|
+
const snapshotMatch = /(?:^|-)(\d{4})-(\d{2})-(\d{2})(?:$|-)/.exec(value);
|
|
493
|
+
const snapshot = snapshotMatch
|
|
494
|
+
? Number(`${snapshotMatch[1]}${snapshotMatch[2]}${snapshotMatch[3]}`)
|
|
495
|
+
: 0;
|
|
496
|
+
const tier = /(?:^|-)sol(?:-|$)/.test(value) ? 60
|
|
497
|
+
: /(?:^|-)pro(?:-|$)/.test(value) ? 50
|
|
498
|
+
: /^gpt-\d+(?:\.\d+)*(?:-\d{4}-\d{2}-\d{2})?$/.test(value) ? 45
|
|
499
|
+
: /(?:^|-)codex(?:-|$)/.test(value) ? 40
|
|
500
|
+
: /(?:^|-)terra(?:-|$)/.test(value) ? 30
|
|
501
|
+
: /(?:^|-)mini(?:-|$)/.test(value) ? 20
|
|
502
|
+
: /(?:^|-)luna(?:-|$)/.test(value) ? 10
|
|
503
|
+
: /(?:^|-)nano(?:-|$)/.test(value) ? 5
|
|
504
|
+
: 0;
|
|
505
|
+
return { version, tier, alias: snapshot ? 0 : 1, snapshot };
|
|
506
|
+
}
|
|
507
|
+
function isRecord(value) {
|
|
508
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
509
|
+
}
|
|
510
|
+
async function fileExists(filePath) {
|
|
511
|
+
try {
|
|
512
|
+
await fs.access(filePath);
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function defaultSpawnDetached(command, args) {
|
|
520
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
521
|
+
child.unref();
|
|
522
|
+
}
|
|
523
|
+
function runProcess(command, args) {
|
|
524
|
+
return new Promise((resolve, reject) => {
|
|
525
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
526
|
+
let stdout = "";
|
|
527
|
+
let stderr = "";
|
|
528
|
+
child.stdout.setEncoding("utf8");
|
|
529
|
+
child.stderr.setEncoding("utf8");
|
|
530
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
531
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
532
|
+
child.once("error", reject);
|
|
533
|
+
child.once("close", (code, signal) => {
|
|
534
|
+
if (signal) {
|
|
535
|
+
reject(new Error(`${command} was interrupted by ${signal}.`));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const CLAUDE_ROOT_PERMISSION_WARNING = "Claude bypass permissions is unavailable while Axiom Runtime runs as root; using default permissions. Run `ai` as a non-root user to enable auto mode.";
|
|
2
|
+
export function getProcessUid() {
|
|
3
|
+
return typeof process.getuid === "function" ? process.getuid() : null;
|
|
4
|
+
}
|
|
5
|
+
export function isPrivilegedProcess(options = {}) {
|
|
6
|
+
const uid = options.uid === undefined ? getProcessUid() : options.uid;
|
|
7
|
+
return uid === 0;
|
|
8
|
+
}
|
|
9
|
+
export function resolveClaudePermissionMode(requestedMode, options = {}) {
|
|
10
|
+
const privileged = isPrivilegedProcess(options);
|
|
11
|
+
const constrained = privileged && requestedMode === "bypassPermissions";
|
|
12
|
+
return {
|
|
13
|
+
requestedMode,
|
|
14
|
+
effectiveMode: constrained ? "default" : requestedMode,
|
|
15
|
+
privileged,
|
|
16
|
+
constrained,
|
|
17
|
+
reason: constrained ? "root_bypass_permissions_unsupported" : null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveClaudePermissionMode } from "./claude-permission-policy.js";
|
|
1
2
|
const runnerEngines = new Map();
|
|
2
3
|
export function registerRunnerEngine(descriptor) {
|
|
3
4
|
runnerEngines.set(descriptor.name, descriptor);
|
|
@@ -30,12 +31,13 @@ registerRunnerEngine({
|
|
|
30
31
|
ANTHROPIC_MODEL: model
|
|
31
32
|
};
|
|
32
33
|
},
|
|
33
|
-
buildArgs: ({ provider, model, settingsPath }) => {
|
|
34
|
+
buildArgs: ({ provider, model, settingsPath, uid }) => {
|
|
34
35
|
const args = ["--model", model];
|
|
35
36
|
if (settingsPath) {
|
|
36
37
|
args.push("--settings", settingsPath);
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
const permission = resolveClaudePermissionMode(provider.mode === "auto" ? "bypassPermissions" : "default", { uid });
|
|
40
|
+
if (permission.effectiveMode === "bypassPermissions") {
|
|
39
41
|
args.push("--permission-mode", "bypassPermissions");
|
|
40
42
|
args.push("--dangerously-skip-permissions");
|
|
41
43
|
}
|
|
@@ -7,6 +7,7 @@ import { writeLog } from "../logs/log-service.js";
|
|
|
7
7
|
import { isRecord } from "../utils/is-record.js";
|
|
8
8
|
import { resolveToolCommand } from "./command-resolver.js";
|
|
9
9
|
import { getRunnerEngine } from "./engine-registry.js";
|
|
10
|
+
import { CLAUDE_ROOT_PERMISSION_WARNING, resolveClaudePermissionMode } from "./claude-permission-policy.js";
|
|
10
11
|
import { finishSession, startSession } from "../sessions/session-service.js";
|
|
11
12
|
export async function runTool(tool, provider, model, options = {}) {
|
|
12
13
|
await saveLastSelection(tool, provider.name, model);
|
|
@@ -15,13 +16,20 @@ export async function runTool(tool, provider, model, options = {}) {
|
|
|
15
16
|
throw new Error(`Failed to find ${tool} CLI. Run \`ai doctor\` to check local CLI installation.`);
|
|
16
17
|
}
|
|
17
18
|
const engine = getRunnerEngine(tool);
|
|
18
|
-
const
|
|
19
|
+
const launchPolicy = resolveRunnerLaunchPolicy(tool, provider);
|
|
20
|
+
if (launchPolicy.warning) {
|
|
21
|
+
console.warn(`Warning: ${launchPolicy.warning}`);
|
|
22
|
+
}
|
|
23
|
+
const effectiveProvider = launchPolicy.provider;
|
|
24
|
+
const env = buildRunnerEnv(tool, effectiveProvider, model);
|
|
19
25
|
const temporaryFiles = [];
|
|
20
|
-
const claudeSettingsPath = engine.requiresSettingsFile
|
|
26
|
+
const claudeSettingsPath = engine.requiresSettingsFile
|
|
27
|
+
? writeTemporaryClaudeSettings(effectiveProvider, { permissionMode: launchPolicy.claudeSettingsPermissionMode })
|
|
28
|
+
: null;
|
|
21
29
|
if (claudeSettingsPath)
|
|
22
30
|
temporaryFiles.push(claudeSettingsPath);
|
|
23
|
-
const args = buildRunnerArgs(tool,
|
|
24
|
-
const effectiveBaseUrl = getToolBaseUrl(tool,
|
|
31
|
+
const args = buildRunnerArgs(tool, effectiveProvider, model, claudeSettingsPath);
|
|
32
|
+
const effectiveBaseUrl = getToolBaseUrl(tool, effectiveProvider);
|
|
25
33
|
await writeLog({
|
|
26
34
|
category: "usage",
|
|
27
35
|
action: "tool_spawn",
|
|
@@ -30,7 +38,9 @@ export async function runTool(tool, provider, model, options = {}) {
|
|
|
30
38
|
tool,
|
|
31
39
|
provider: provider.name,
|
|
32
40
|
model,
|
|
33
|
-
mode:
|
|
41
|
+
mode: effectiveProvider.mode,
|
|
42
|
+
configuredMode: provider.mode,
|
|
43
|
+
permissionConstraint: launchPolicy.reason,
|
|
34
44
|
baseUrl: effectiveBaseUrl,
|
|
35
45
|
configuredBaseUrl: provider.baseUrl,
|
|
36
46
|
apiKeyFingerprint: getSecretFingerprint(provider.apiKey),
|
|
@@ -41,7 +51,7 @@ export async function runTool(tool, provider, model, options = {}) {
|
|
|
41
51
|
});
|
|
42
52
|
const session = await startSession({
|
|
43
53
|
tool,
|
|
44
|
-
provider,
|
|
54
|
+
provider: effectiveProvider,
|
|
45
55
|
model,
|
|
46
56
|
projectPath: process.cwd(),
|
|
47
57
|
fallbackFrom: options.fallbackFrom ?? null
|
|
@@ -70,8 +80,27 @@ export function getSecretFingerprint(value) {
|
|
|
70
80
|
return `len=${value.length}`;
|
|
71
81
|
return `${value.slice(0, 8)}...${value.slice(-6)} len=${value.length}`;
|
|
72
82
|
}
|
|
73
|
-
export function buildRunnerArgs(tool, provider, model, claudeSettingsPath) {
|
|
74
|
-
|
|
83
|
+
export function buildRunnerArgs(tool, provider, model, claudeSettingsPath, identity = {}) {
|
|
84
|
+
const launchPolicy = resolveRunnerLaunchPolicy(tool, provider, identity);
|
|
85
|
+
return getRunnerEngine(tool).buildArgs({
|
|
86
|
+
provider: launchPolicy.provider,
|
|
87
|
+
model,
|
|
88
|
+
settingsPath: claudeSettingsPath,
|
|
89
|
+
uid: identity.uid
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function resolveRunnerLaunchPolicy(tool, provider, identity = {}) {
|
|
93
|
+
if (tool !== "claude") {
|
|
94
|
+
return { provider, claudeSettingsPermissionMode: "provider", warning: null, reason: null };
|
|
95
|
+
}
|
|
96
|
+
const requestedMode = provider.mode === "auto" ? "bypassPermissions" : "default";
|
|
97
|
+
const resolution = resolveClaudePermissionMode(requestedMode, identity);
|
|
98
|
+
return {
|
|
99
|
+
provider: resolution.constrained ? { ...provider, mode: "ask" } : provider,
|
|
100
|
+
claudeSettingsPermissionMode: resolution.privileged ? "safeDefault" : "provider",
|
|
101
|
+
warning: resolution.constrained ? CLAUDE_ROOT_PERMISSION_WARNING : null,
|
|
102
|
+
reason: resolution.reason
|
|
103
|
+
};
|
|
75
104
|
}
|
|
76
105
|
const AUTO_PERMISSIONS = {
|
|
77
106
|
defaultMode: "bypassPermissions",
|
|
@@ -94,6 +123,11 @@ const READ_ONLY_PERMISSIONS = {
|
|
|
94
123
|
allow: ["Read", "Glob", "Grep", "WebFetch", "WebSearch"],
|
|
95
124
|
deny: ["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]
|
|
96
125
|
};
|
|
126
|
+
const SAFE_DEFAULT_PERMISSIONS = {
|
|
127
|
+
defaultMode: "default",
|
|
128
|
+
allow: [],
|
|
129
|
+
deny: []
|
|
130
|
+
};
|
|
97
131
|
export function writeTemporaryClaudeSettings(provider, options = {}) {
|
|
98
132
|
const userSettingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
99
133
|
const settings = readClaudeSettings(userSettingsPath);
|
|
@@ -104,7 +138,10 @@ export function writeTemporaryClaudeSettings(provider, options = {}) {
|
|
|
104
138
|
ANTHROPIC_API_KEY: provider.apiKey,
|
|
105
139
|
ANTHROPIC_BASE_URL: baseUrl
|
|
106
140
|
};
|
|
107
|
-
if (options.permissionMode === "
|
|
141
|
+
if (options.permissionMode === "safeDefault") {
|
|
142
|
+
settings.permissions = { ...SAFE_DEFAULT_PERMISSIONS };
|
|
143
|
+
}
|
|
144
|
+
else if (options.permissionMode === "readOnly") {
|
|
108
145
|
settings.permissions = { ...READ_ONLY_PERMISSIONS };
|
|
109
146
|
}
|
|
110
147
|
else if (provider.mode === "auto") {
|
|
@@ -8,6 +8,7 @@ import { readProviders } from "../config/providers-store.js";
|
|
|
8
8
|
import { checkProvider, runDeepCheck } from "../models/model-discovery.js";
|
|
9
9
|
import { writeLog } from "../logs/log-service.js";
|
|
10
10
|
import { getKnownCandidates, resolveToolCommand } from "../runner/command-resolver.js";
|
|
11
|
+
import { isPrivilegedProcess } from "../runner/claude-permission-policy.js";
|
|
11
12
|
import { resolveProviderCandidatesForTool } from "../runner/fallback.js";
|
|
12
13
|
import { buildUsageEvent } from "../usage/usage-service.js";
|
|
13
14
|
const MIN_NODE_VERSION = "22.19.0";
|
|
@@ -16,6 +17,7 @@ export async function runDoctor(options = {}) {
|
|
|
16
17
|
const fix = options.fix ?? false;
|
|
17
18
|
const checkProviders = options.checkProviders ?? true;
|
|
18
19
|
checkNodeVersion(items);
|
|
20
|
+
checkExecutionUser(items);
|
|
19
21
|
if (options.checkNativeDependencies ?? true) {
|
|
20
22
|
await checkNativeDependencies(items);
|
|
21
23
|
}
|
|
@@ -26,6 +28,21 @@ export async function runDoctor(options = {}) {
|
|
|
26
28
|
checkCommand("Local CLIs", "codex", items);
|
|
27
29
|
return items;
|
|
28
30
|
}
|
|
31
|
+
function checkExecutionUser(items) {
|
|
32
|
+
const item = getExecutionUserDoctorItem();
|
|
33
|
+
if (item)
|
|
34
|
+
items.push(item);
|
|
35
|
+
}
|
|
36
|
+
export function getExecutionUserDoctorItem(identity = {}) {
|
|
37
|
+
if (!isPrivilegedProcess(identity))
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
group: "Execution User",
|
|
41
|
+
status: "warn",
|
|
42
|
+
message: "Axiom Runtime is running as root; Claude auto/bypass permission mode is unavailable and will be constrained to default permissions.",
|
|
43
|
+
suggestion: "Install globally as root if required, but run `ai` and long-lived services with a dedicated non-root account."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
29
46
|
export async function runDeepDoctor(tool = "codex", options = {}) {
|
|
30
47
|
const items = await runDoctor(options);
|
|
31
48
|
try {
|
|
@@ -3,6 +3,7 @@ import fsPromises from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { getConfigRoot, getTelegramBotConfigPath, getTelegramBotDbPath, getTelegramBotDir, getTelegramBotLogPath, getTelegramBotPidPath, getTelegramConfigPath, getTelegramDbPath } from "../core/config/paths.js";
|
|
5
5
|
const BOT_NAME_RE = /^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/;
|
|
6
|
+
const LEGACY_MIGRATION_MARKER = ".telegram-legacy-migrated";
|
|
6
7
|
export function isValidBotName(name) {
|
|
7
8
|
return BOT_NAME_RE.test(name) && name.length <= 32;
|
|
8
9
|
}
|
|
@@ -42,13 +43,19 @@ export async function migrateLegacyConfig() {
|
|
|
42
43
|
const legacyPath = getTelegramConfigPath();
|
|
43
44
|
if (!fs.existsSync(legacyPath))
|
|
44
45
|
return;
|
|
45
|
-
if (
|
|
46
|
+
if (fs.existsSync(getLegacyMigrationMarkerPath()))
|
|
46
47
|
return;
|
|
47
48
|
const dir = getTelegramBotDir();
|
|
48
49
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
49
|
-
|
|
50
|
+
if (!botExists("default")) {
|
|
51
|
+
await fsPromises.copyFile(legacyPath, getTelegramBotConfigPath("default"));
|
|
52
|
+
}
|
|
53
|
+
await markLegacyMigrationComplete();
|
|
50
54
|
}
|
|
51
55
|
export async function removeBotFiles(name, configuredDbPath) {
|
|
56
|
+
if (name === "default") {
|
|
57
|
+
await markLegacyMigrationComplete();
|
|
58
|
+
}
|
|
52
59
|
const managedConfiguredDbPath = configuredDbPath && isManagedConfigPath(configuredDbPath)
|
|
53
60
|
? path.resolve(configuredDbPath)
|
|
54
61
|
: undefined;
|
|
@@ -61,8 +68,7 @@ export async function removeBotFiles(name, configuredDbPath) {
|
|
|
61
68
|
getTelegramBotConfigPath(name),
|
|
62
69
|
getTelegramBotPidPath(name),
|
|
63
70
|
getTelegramBotLogPath(name),
|
|
64
|
-
...[...dbPaths].flatMap((dbPath) => [dbPath, `${dbPath}-shm`, `${dbPath}-wal`])
|
|
65
|
-
...(name === "default" ? [getTelegramConfigPath()] : [])
|
|
71
|
+
...[...dbPaths].flatMap((dbPath) => [dbPath, `${dbPath}-shm`, `${dbPath}-wal`])
|
|
66
72
|
];
|
|
67
73
|
for (const p of paths) {
|
|
68
74
|
try {
|
|
@@ -78,3 +84,10 @@ function isManagedConfigPath(filePath) {
|
|
|
78
84
|
const relative = path.relative(getConfigRoot(), path.resolve(filePath));
|
|
79
85
|
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
80
86
|
}
|
|
87
|
+
function getLegacyMigrationMarkerPath() {
|
|
88
|
+
return path.join(getConfigRoot(), LEGACY_MIGRATION_MARKER);
|
|
89
|
+
}
|
|
90
|
+
async function markLegacyMigrationComplete() {
|
|
91
|
+
await fsPromises.mkdir(getConfigRoot(), { recursive: true });
|
|
92
|
+
await fsPromises.writeFile(getLegacyMigrationMarkerPath(), "completed\n", { mode: 0o600 });
|
|
93
|
+
}
|
|
@@ -3,13 +3,15 @@ import { createJsonLineAccumulator, extractJsonLines, spawnCapture, stripAnsi, t
|
|
|
3
3
|
import { requiresDangerousConfirmation } from "../interaction/approval.js";
|
|
4
4
|
import { writeTelegramLog } from "../log.js";
|
|
5
5
|
import { isRecord } from "../../core/utils/is-record.js";
|
|
6
|
+
import { CLAUDE_ROOT_PERMISSION_WARNING, resolveClaudePermissionMode } from "../../core/runner/claude-permission-policy.js";
|
|
6
7
|
export class ClaudeEngine {
|
|
7
8
|
name = "claude";
|
|
8
9
|
processes = new Map();
|
|
9
10
|
controllers = new Map();
|
|
10
11
|
async execute(params) {
|
|
11
|
-
const
|
|
12
|
-
const runtime = await prepareEngineRuntime("claude", params.providerName, params.modelName,
|
|
12
|
+
const requestedPermissionMode = params.permissionMode ?? "default";
|
|
13
|
+
const runtime = await prepareEngineRuntime("claude", params.providerName, params.modelName, requestedPermissionMode);
|
|
14
|
+
const permissionMode = runtime.claudePermission?.effectiveMode ?? requestedPermissionMode;
|
|
13
15
|
const controller = new AbortController();
|
|
14
16
|
this.controllers.set(params.chatId, controller);
|
|
15
17
|
const signal = params.signal ?? controller.signal;
|
|
@@ -29,11 +31,25 @@ export class ClaudeEngine {
|
|
|
29
31
|
resume: params.resume,
|
|
30
32
|
allowedTools: params.allowedTools
|
|
31
33
|
});
|
|
32
|
-
|
|
34
|
+
const permissionWarning = runtime.claudePermission?.constrained ? CLAUDE_ROOT_PERMISSION_WARNING : null;
|
|
35
|
+
await params.onProgress?.({ phase: "thinking", message: permissionWarning ?? "Claude started" });
|
|
36
|
+
if (permissionWarning) {
|
|
37
|
+
await writeTelegramLog({
|
|
38
|
+
level: "warn",
|
|
39
|
+
action: "claude_permission_constrained",
|
|
40
|
+
message: permissionWarning,
|
|
41
|
+
metadata: { requestedPermissionMode, effectivePermissionMode: permissionMode, reason: runtime.claudePermission?.reason }
|
|
42
|
+
});
|
|
43
|
+
}
|
|
33
44
|
await writeTelegramLog({
|
|
34
45
|
action: "claude_start",
|
|
35
46
|
message: `Starting Claude for Telegram chat ${params.chatId}`,
|
|
36
|
-
metadata: {
|
|
47
|
+
metadata: {
|
|
48
|
+
...summarizeRuntime(runtime.provider, runtime.model, "claude"),
|
|
49
|
+
cwd: params.cwd,
|
|
50
|
+
requestedPermissionMode,
|
|
51
|
+
effectivePermissionMode: permissionMode
|
|
52
|
+
}
|
|
37
53
|
});
|
|
38
54
|
try {
|
|
39
55
|
const progressParser = createJsonLineAccumulator((event) => handleClaudeProgressEvent(event, params.onProgress));
|
|
@@ -89,7 +105,8 @@ export class ClaudeEngine {
|
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
107
|
export function buildClaudeArgs(options) {
|
|
92
|
-
const
|
|
108
|
+
const resolution = resolveClaudePermissionMode(options.permissionMode, { uid: options.uid });
|
|
109
|
+
const permissionMode = resolution.effectiveMode === "readOnly" ? "default" : resolution.effectiveMode;
|
|
93
110
|
const args = [
|
|
94
111
|
"-p",
|
|
95
112
|
options.prompt,
|
|
@@ -111,7 +128,7 @@ export function buildClaudeArgs(options) {
|
|
|
111
128
|
if (options.allowedTools?.length) {
|
|
112
129
|
args.push("--allowedTools", options.allowedTools.join(","));
|
|
113
130
|
}
|
|
114
|
-
if (
|
|
131
|
+
if (resolution.effectiveMode === "bypassPermissions") {
|
|
115
132
|
args.push("--dangerously-skip-permissions");
|
|
116
133
|
}
|
|
117
134
|
if (options.permissionMode === "readOnly") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { resolveExactProviderForTool, resolveProviderForTool } from "../../core/runner/fallback.js";
|
|
3
3
|
import { buildRunnerEnv, cleanupTemporaryFiles, getToolBaseUrl, writeTemporaryClaudeSettings } from "../../core/runner/tool-runner.js";
|
|
4
|
+
import { resolveClaudePermissionMode } from "../../core/runner/claude-permission-policy.js";
|
|
4
5
|
import { resolveToolCommand } from "../../core/runner/command-resolver.js";
|
|
5
6
|
export async function prepareEngineRuntime(tool, providerName, modelName, permissionMode) {
|
|
6
7
|
// Telegram resolves and orders fallback candidates before invoking an
|
|
@@ -13,17 +14,27 @@ export async function prepareEngineRuntime(tool, providerName, modelName, permis
|
|
|
13
14
|
if (!command) {
|
|
14
15
|
throw new Error(`Failed to find ${tool} CLI. Run \`ai doctor\` to check local CLI installation.`);
|
|
15
16
|
}
|
|
17
|
+
const claudePermission = tool === "claude"
|
|
18
|
+
? resolveClaudePermissionMode(permissionMode ?? "default")
|
|
19
|
+
: null;
|
|
20
|
+
const effectiveProvider = claudePermission?.constrained ? { ...provider, mode: "ask" } : provider;
|
|
16
21
|
const env = {
|
|
17
|
-
...buildRunnerEnv(tool,
|
|
22
|
+
...buildRunnerEnv(tool, effectiveProvider, model),
|
|
18
23
|
AI_GATEWAY_SOURCE: "telegram"
|
|
19
24
|
};
|
|
20
25
|
const temporaryFiles = [];
|
|
21
26
|
const claudeSettingsPath = tool === "claude"
|
|
22
|
-
? writeTemporaryClaudeSettings(provider, {
|
|
27
|
+
? writeTemporaryClaudeSettings(provider, {
|
|
28
|
+
permissionMode: permissionMode === "readOnly"
|
|
29
|
+
? "readOnly"
|
|
30
|
+
: claudePermission?.privileged
|
|
31
|
+
? "safeDefault"
|
|
32
|
+
: "provider"
|
|
33
|
+
})
|
|
23
34
|
: null;
|
|
24
35
|
if (claudeSettingsPath)
|
|
25
36
|
temporaryFiles.push(claudeSettingsPath);
|
|
26
|
-
return { provider, model, env, command, temporaryFiles, claudeSettingsPath };
|
|
37
|
+
return { provider, model, env, command, temporaryFiles, claudeSettingsPath, claudePermission };
|
|
27
38
|
}
|
|
28
39
|
export function cleanupRuntime(runtime) {
|
|
29
40
|
cleanupTemporaryFiles(runtime.temporaryFiles);
|
package/docs/USAGE.html
CHANGED
|
@@ -458,13 +458,16 @@
|
|
|
458
458
|
<section id="install" class="panel">
|
|
459
459
|
<h2>通过 npm 安装</h2>
|
|
460
460
|
<div class="detail-body">
|
|
461
|
-
<pre>
|
|
461
|
+
<pre>node --version
|
|
462
|
+
npm install --global @offerpilot/axiomruntime
|
|
462
463
|
ai --version
|
|
463
464
|
ai setup</pre>
|
|
464
465
|
<p>npm 包名是 <code>@offerpilot/axiomruntime</code>,安装后注册的可执行命令是 <code>ai</code>。需要 Node.js <code>22.19.0+</code>。</p>
|
|
466
|
+
<p>如果 npm 显示 <code>EBADENGINE</code>,请先升级 Node.js 再重新安装,不要继续使用警告状态下安装的 CLI。</p>
|
|
465
467
|
<p><code>ai --version</code> 无副作用地验证安装;<code>ai setup</code> 初始化 Runtime,并可在确认后安装缺失的 Claude Code/Codex。</p>
|
|
466
|
-
<
|
|
467
|
-
brew
|
|
468
|
+
<p>全局安装可以由 root 完成,但日常执行和常驻服务应使用专用非 root 用户。root 下请求 Claude <code>auto</code>/<code>bypassPermissions</code> 时,Runtime 会安全约束为默认权限并告警,不会传递 <code>--dangerously-skip-permissions</code>。</p>
|
|
469
|
+
<pre>brew tap linlangli/axiom https://gitlab.com/linlangli/axiom.git
|
|
470
|
+
brew install linlangli/axiom/axiomruntime
|
|
468
471
|
ai --version
|
|
469
472
|
ai setup</pre>
|
|
470
473
|
<p>Homebrew Formula 复用同版本 npm tarball并校验 SHA256,固定使用 Homebrew <code>node@22</code>。非 GitHub Tap 首次添加时需提供公开 Git URL。</p>
|
|
@@ -534,6 +537,12 @@ ai setup</pre>
|
|
|
534
537
|
<td>指定 provider 和模型启动工具。</td>
|
|
535
538
|
<td>provider 不可用时按 level 降级。</td>
|
|
536
539
|
</tr>
|
|
540
|
+
<tr data-search="set codex chatgpt login oauth provider proxy config auth restart app">
|
|
541
|
+
<td><span class="group-label">使用</span></td>
|
|
542
|
+
<td><code class="cmd">ai set codex chatgpt</code><br /><code class="cmd">ai set codex provider 51talk-cq gpt-5.6-sol</code><br /><code class="cmd">ai set codex provider 51talk-cq</code><br /><code class="cmd">ai set codex provider</code><br /><code class="cmd">ai set codex proxy 127.0.0.1:7890</code></td>
|
|
543
|
+
<td>持久切换 Codex App 的 ChatGPT 账号、已添加 provider 或 HTTP proxy。</td>
|
|
544
|
+
<td>一致更新配置与必要环境,成功后重启 app。</td>
|
|
545
|
+
</tr>
|
|
537
546
|
<tr data-search="last resume previous selection">
|
|
538
547
|
<td><span class="group-label">使用</span></td>
|
|
539
548
|
<td><code class="cmd">ai last</code></td>
|
|
@@ -556,7 +565,7 @@ ai setup</pre>
|
|
|
556
565
|
<td><span class="group-label">Provider</span></td>
|
|
557
566
|
<td><code class="cmd">ai provider edit</code><br /><code class="cmd">ai provider edit 51talk</code></td>
|
|
558
567
|
<td>修改 name、baseUrl、凭证、mode、level、model。</td>
|
|
559
|
-
<td
|
|
568
|
+
<td>当前 apiKey 以明文保存在 providers.json,编辑时会回显当前值。SecretStore + apiKeyRef 是目标态,尚未实现。</td>
|
|
560
569
|
</tr>
|
|
561
570
|
<tr data-search="provider delete remove fallback">
|
|
562
571
|
<td><span class="group-label">Provider</span></td>
|
|
@@ -628,13 +637,35 @@ ai use codex 51talk gpt-5.5</pre>
|
|
|
628
637
|
<ul>
|
|
629
638
|
<li>provider 不可用时按 <code>level</code> 自动降级。</li>
|
|
630
639
|
<li>Claude Code 使用临时 <code>--settings</code> 注入环境变量,不修改用户 settings。</li>
|
|
631
|
-
<li>
|
|
632
|
-
<li>provider 的 <code>mode</code> 为 <code>auto</code>
|
|
640
|
+
<li><code>ai use codex</code> 使用本次子进程的 <code>-c model_provider=...</code> 固定所选 provider,并通过 <code>env_key = "OPENAI_API_KEY"</code> 使用 AI Gateway 注入的 Key;不会修改 Codex 全局认证或配置。</li>
|
|
641
|
+
<li>provider 的 <code>mode</code> 为 <code>auto</code> 时,非 root 执行 Claude Code 会追加 <code>--dangerously-skip-permissions</code>;<code>ask</code> 模式不会追加,并会在启动前确认。root 下请求 <code>auto</code> 会被安全约束为默认权限,临时 settings 中继承的 bypass 配置也会被清除。</li>
|
|
633
642
|
<li>OpenAI 兼容地址用于 Codex 时会使用 <code>/v1</code>,用于 Claude 时会去掉 <code>/v1</code>。</li>
|
|
634
643
|
<li>日志只记录 Key 指纹,不记录完整 API Key。</li>
|
|
635
644
|
</ul>
|
|
636
645
|
</div>
|
|
637
646
|
</div>
|
|
647
|
+
<details open>
|
|
648
|
+
<summary>持久设置 Codex App provider</summary>
|
|
649
|
+
<div class="detail-body">
|
|
650
|
+
<pre>ai set codex chatgpt
|
|
651
|
+
ai set codex provider 51talk-cq gpt-5.6-sol
|
|
652
|
+
ai set codex provider 51talk-cq
|
|
653
|
+
ai set codex provider
|
|
654
|
+
ai set codex proxy 127.0.0.1:7890
|
|
655
|
+
ai set codex proxy off</pre>
|
|
656
|
+
<ul>
|
|
657
|
+
<li><code>chatgpt</code> 调用 <code>codex login</code> 打开浏览器,并在成功后选择内建 <code>model_provider = "openai"</code>。</li>
|
|
658
|
+
<li><code>provider name model-id</code> 显式选择该 provider 已探测的 <code>gpt-*</code> 模型;只给 provider 时自动选最新 GPT。</li>
|
|
659
|
+
<li><code>provider</code> 不带名称时依次交互选择已添加的 provider 和 GPT 模型;没有可用 GPT 时提示运行 <code>ai status</code>,不修改配置。</li>
|
|
660
|
+
<li>自动选择先比较 GPT 数字版本,同版本优先 <code>sol</code>、<code>pro</code>、alias、<code>codex</code>、<code>terra</code>、<code>mini</code>、<code>luna</code>、<code>nano</code>,并优先滚动 alias。API Key 只写入 mode <code>0600</code> 的 <code>auth.json</code>。</li>
|
|
661
|
+
<li>原有 project、MCP、plugin、feature 设置会保留;任一步失败会恢复原 <code>config.toml</code> 和 <code>auth.json</code>。</li>
|
|
662
|
+
<li>切换自定义 provider 会替换当前 ChatGPT token;切回 ChatGPT 时需重新登录。</li>
|
|
663
|
+
<li>成功后 macOS 会重启 <code>Codex.app</code> 或当前名称的 <code>ChatGPT.app</code>;无法自动重启时按提示手动操作。</li>
|
|
664
|
+
<li><code>proxy</code> 把裸 <code>host:port</code> 规范化为 HTTP origin,只更新 <code>shell_environment_policy.set</code> 的大小写 HTTP(S)/ALL proxy keys;<code>off</code> 清除这些 managed keys,不修改 <code>auth.json</code>。</li>
|
|
665
|
+
<li>Proxy URL 禁止包含账号密码、path、query 或 fragment,command usage log 会整体脱敏。macOS 会同步当前登录会话的 user launchd environment;失败时恢复原 config 且不重启,注销或重启 macOS 后需重新运行本命令。</li>
|
|
666
|
+
</ul>
|
|
667
|
+
</div>
|
|
668
|
+
</details>
|
|
638
669
|
</section>
|
|
639
670
|
|
|
640
671
|
<section id="memory" class="panel">
|
|
@@ -712,7 +743,7 @@ ai telegram status --all</pre>
|
|
|
712
743
|
<li><code>--bot=<name></code> 与 <code>--bot <name></code> 等价。旧位置参数写法继续兼容;新脚本推荐显式指定 <code>--bot</code> 或 <code>--all</code>。</li>
|
|
713
744
|
<li>裸 <code>ai telegram</code> 会先选操作再列出 Bot;生命周期菜单可选 <code>all bots</code>,删除菜单会二次确认。带 <code>--bot</code> 进入交互菜单时会跳过 Bot 选择。</li>
|
|
714
745
|
<li><code>edit</code> 交互式编辑指定 Bot;<code>delete</code> 先停止目标 Bot,再删除该 Bot 的 config、PID、log 和 SQLite 文件。旧 <code>remove</code> 是兼容别名。</li>
|
|
715
|
-
<li
|
|
746
|
+
<li>legacy <code>telegram.json</code> 首次会自动迁移为命名 Bot <code>default</code>;删除 <code>default</code> 后,列表、生命周期命令和 Bot 服务不会再从 legacy 配置重建它。无目标 <code>ai telegram config</code> 仍可管理 legacy global config。</li>
|
|
716
747
|
<li><code>config set ... --bot <name></code> 只修改指定 Bot,不影响全局或其他 Bot;token 不会出现在命令日志或终端输出中。</li>
|
|
717
748
|
<li>命名 Bot 的配置、PID、日志和会话保存在 <code>~/.ai-gateway/telegram/bots/<name>.json|.pid|.log|.sqlite</code>。</li>
|
|
718
749
|
<li>为保持兼容,存在命名 Bot 时,无目标 <code>start</code>、<code>stop</code>、<code>status</code> 仍作用于全部命名 Bot。</li>
|
|
@@ -809,7 +840,8 @@ ai session clear</pre>
|
|
|
809
840
|
<pre>~/.ai-gateway/providers.json
|
|
810
841
|
~/.ai-gateway/memory.json
|
|
811
842
|
~/.ai-gateway/model-pricing.json</pre>
|
|
812
|
-
<p><code>providers.json</code> 保存聊天 provider
|
|
843
|
+
<p><code>providers.json</code> 保存聊天 provider;<code>memory.json</code> 保存记忆检索 embedding 模型。<code>model-pricing.json</code> 保存美元 / 百万 token 的模型价格。</p>
|
|
844
|
+
<p><strong>注意:</strong>这两个文件当前都以明文保存 <code>apiKey</code>。请限制文件权限并且不要提交到仓库。凭证改由 SecretStore 管理、配置只留 <code>apiKeyRef</code> 是目标架构,尚未实现。</p>
|
|
813
845
|
</div>
|
|
814
846
|
</details>
|
|
815
847
|
<details>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@offerpilot/axiomruntime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "CLI-first foundation for an enterprise AI Runtime with provider routing and governed execution.",
|
|
5
5
|
"homepage": "https://gitlab.com/linlangli/agent-router",
|
|
6
6
|
"repository": {
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
"telegram:service": "node --enable-source-maps dist/telegram/supervisor.js",
|
|
23
23
|
"test": "npm run build && node --test test/*.test.js",
|
|
24
24
|
"release": "node scripts/release.js",
|
|
25
|
-
"release:current": "node scripts/release.js --
|
|
26
|
-
"release:check": "npm test && npm run
|
|
25
|
+
"release:current": "node scripts/release.js --npm",
|
|
26
|
+
"release:check": "npm test && npm pack --dry-run",
|
|
27
27
|
"homebrew:update": "node scripts/update-homebrew-formula.js --write",
|
|
28
28
|
"homebrew:check": "node scripts/update-homebrew-formula.js",
|
|
29
|
+
"homebrew:publish": "node scripts/release.js --homebrew",
|
|
29
30
|
"prepublishOnly": "npm run release:check"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
@@ -47,11 +48,11 @@
|
|
|
47
48
|
"typescript": "^5.8.0"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
|
-
"better-sqlite3": "^
|
|
51
|
+
"better-sqlite3": "^13.0.2",
|
|
51
52
|
"gpt-tokenizer": "^3.4.0",
|
|
52
53
|
"grammy": "^1.44.0",
|
|
53
54
|
"https-proxy-agent": "^9.1.0",
|
|
54
55
|
"proper-lockfile": "^4.1.2",
|
|
55
|
-
"undici": "^8.
|
|
56
|
+
"undici": "^8.10.0"
|
|
56
57
|
}
|
|
57
58
|
}
|