abelworkflow 0.9.0 → 0.9.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 CHANGED
@@ -82,7 +82,7 @@ npx abelworkflow@latest
82
82
  > - 一键安装或更新 `Claude Code`、`Codex`、`Pi`
83
83
  > - 配置 `Claude Code` 的第三方 API 到 `~/.claude/settings.json`
84
84
  > - 配置 `Codex` 的第三方 API 到 `~/.codex/config.toml` 和 `~/.codex/auth.json`
85
- > - 配置 `Pi` 的自定义 API 到 `~/.pi/agent/models.json` 中的 `gpt` provider,并设置 `~/.pi/agent/settings.json` 默认模型,同时携带 Pi 扩展
85
+ > - 配置 `Pi` 的自定义 API 到 `~/.pi/agent/models.json` 和 `~/.pi/agent/auth.json` 中的 `gpt` provider,并设置 `~/.pi/agent/settings.json` 默认模型,同时携带 Pi 扩展
86
86
  > - 非交互场景请显式使用 `npx abelworkflow install`,不再保留旧的默认自动同步逻辑。
87
87
 
88
88
  ### 交互式初始化能力
@@ -108,6 +108,23 @@ npx abelworkflow --help
108
108
  8. 可选配置 Pi `gpt` provider 自定义 API,并链接 Pi 扩展
109
109
  9. 可选填写三个技能的环境变量
110
110
 
111
+ ### 自签名 HTTPS 中转
112
+
113
+ 默认不会关闭 TLS 证书校验。自签名中转应优先取得其 CA PEM,并在启动客户端前设置:
114
+
115
+ ```bash
116
+ export NODE_EXTRA_CA_CERTS=/absolute/path/relay-ca.pem
117
+ export CODEX_CA_CERTIFICATE=/absolute/path/relay-ca.pem
118
+ ```
119
+
120
+ - `NODE_EXTRA_CA_CERTS` 供 Claude Code 和 Pi 使用,必须在启动进程前设置。
121
+ - `CODEX_CA_CERTIFICATE` 供 Codex 原生客户端使用;也可使用 `SSL_CERT_FILE`。
122
+ - Claude Code 的配置向导提供显式“不安全 TLS”选项,但它会影响该 Claude Code 进程内的全部 HTTPS 请求。
123
+ - Pi 的配置向导可仅为配置的 `gpt` 请求启用不安全 TLS;扩展会移除内部标记,不影响其他请求。
124
+ - Codex 没有已验证的跳过证书校验配置。无法提供可信 PEM 时,只能使用中转明确提供的 `http://` 地址或受控的本地反向代理。
125
+
126
+ 证书过期或主机名不匹配时应重新签发证书,不能用附加 CA 安全修复。不要在 shell 配置中全局设置 `NODE_TLS_REJECT_UNAUTHORIZED=0`。
127
+
111
128
  ### 技能环境写入位置
112
129
 
113
130
  交互式配置会把技能密钥写到 `~/.agents` 下对应 skill 目录的 `.env` 中:
@@ -183,7 +200,7 @@ node .\bin\abelworkflow.mjs install
183
200
  | `AGENTS.md` | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | `~/.pi/agent/AGENTS.md` | 全局系统提示词/规则 |
184
201
  | `skills/<skill>/` | `~/.claude/skills/<skill>/` | `~/.codex/skills/<skill>/` | `~/.pi/agent/skills/<skill>/` | Skills(每个目录一个技能) |
185
202
  | `commands/abel-*.md` | `~/.claude/commands/abel-*.md` | `~/.codex/prompts/abel-*.md` | `~/.pi/agent/prompts/abel-*.md` | 扁平化部署,避免命名冲突 |
186
- | `extensions/*.ts` | - | - | `~/.pi/agent/extensions/*.ts` | Pi 扩展 |
203
+ | `extensions/*` | - | - | `~/.pi/agent/extensions/*` | Pi 扩展文件或目录 |
187
204
 
188
205
  ### 验证(可选)
189
206
 
@@ -1,6 +1,23 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { createNodeInsecureDispatcher, installProviderTlsFetch } from "./tls-fetch.mjs";
3
+
4
+ let nodeInsecureDispatcher: unknown;
5
+
6
+ function getNodeInsecureDispatcher() {
7
+ if (nodeInsecureDispatcher) return nodeInsecureDispatcher;
8
+ nodeInsecureDispatcher = createNodeInsecureDispatcher();
9
+ return nodeInsecureDispatcher;
10
+ }
2
11
 
3
12
  export default function (pi: ExtensionAPI) {
13
+ if (typeof globalThis.fetch === "function") {
14
+ const runtime = typeof (globalThis as any).Bun === "undefined" ? "node" : "bun";
15
+ installProviderTlsFetch({
16
+ runtime,
17
+ insecureDispatcher: runtime === "node" ? getNodeInsecureDispatcher : undefined
18
+ });
19
+ }
20
+
4
21
  pi.on("before_provider_request", (event, ctx) => {
5
22
  const payload = event.payload as any;
6
23
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
@@ -0,0 +1,188 @@
1
+ export const piInsecureTlsHeader = "x-abelworkflow-insecure-tls";
2
+
3
+ const installedFetchMarker = Symbol.for("abelworkflow.pi-provider-tls-fetch");
4
+ const undiciGlobalDispatchers = [
5
+ Symbol.for("undici.globalDispatcher.2"),
6
+ Symbol.for("undici.globalDispatcher.1")
7
+ ];
8
+ const redirectStatuses = new Set([301, 302, 303, 307, 308]);
9
+ const maxRedirects = 5;
10
+
11
+ function getMarkedHeaders(input, init) {
12
+ const source = init?.headers !== undefined
13
+ ? init.headers
14
+ : typeof Request !== "undefined" && input instanceof Request
15
+ ? input.headers
16
+ : undefined;
17
+ if (source === undefined) return null;
18
+
19
+ const headers = new Headers(source);
20
+ if (!headers.has(piInsecureTlsHeader)) return null;
21
+ const allowedOrigin = headers.get(piInsecureTlsHeader);
22
+ headers.delete(piInsecureTlsHeader);
23
+ return { allowedOrigin, headers };
24
+ }
25
+
26
+ function getRequestUrl(input) {
27
+ return typeof Request !== "undefined" && input instanceof Request
28
+ ? input.url
29
+ : input instanceof URL
30
+ ? input.href
31
+ : input;
32
+ }
33
+
34
+ function getRequestOrigin(input) {
35
+ try {
36
+ return new URL(getRequestUrl(input)).origin;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function redirectRequestInit(input, init, status) {
43
+ const method = String(init.method ?? (
44
+ typeof Request !== "undefined" && input instanceof Request ? input.method : "GET"
45
+ )).toUpperCase();
46
+ const switchToGet = status === 303 && method !== "HEAD"
47
+ || (status === 301 || status === 302) && method === "POST";
48
+ if (!switchToGet) {
49
+ if (typeof Request !== "undefined" && input instanceof Request && method !== "GET" && method !== "HEAD") {
50
+ throw new Error("Pi insecure TLS cannot safely replay a redirected Request body");
51
+ }
52
+ return init;
53
+ }
54
+
55
+ const headers = new Headers(init.headers);
56
+ for (const name of ["content-encoding", "content-language", "content-length", "content-location", "content-type"]) {
57
+ headers.delete(name);
58
+ }
59
+ const nextInit = { ...init, method: "GET", headers };
60
+ delete nextInit.body;
61
+ return nextInit;
62
+ }
63
+
64
+ async function fetchWithSameOriginRedirects({ fetchOnce, input, init, allowedOrigin }) {
65
+ let nextInput = input;
66
+ let nextInit = init;
67
+
68
+ for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount += 1) {
69
+ const response = await fetchOnce(nextInput, nextInit);
70
+ const location = redirectStatuses.has(response?.status) ? response.headers?.get?.("location") : null;
71
+ if (!location) return response;
72
+ if (redirectCount === maxRedirects) {
73
+ await response.body?.cancel?.();
74
+ throw new Error(`Pi insecure TLS exceeded ${maxRedirects} same-origin redirects`);
75
+ }
76
+
77
+ const target = new URL(location, getRequestUrl(nextInput));
78
+ if (target.origin !== allowedOrigin) {
79
+ await response.body?.cancel?.();
80
+ throw new Error(`Pi insecure TLS blocked cross-origin redirect to ${target.origin}`);
81
+ }
82
+ await response.body?.cancel?.();
83
+ nextInit = redirectRequestInit(nextInput, nextInit, response.status);
84
+ nextInput = target.href;
85
+ }
86
+ }
87
+
88
+ function applyNodeDispatcher(fetchImpl, input, init, dispatcherSource) {
89
+ let dispatcher;
90
+ try {
91
+ dispatcher = typeof dispatcherSource === "function" ? dispatcherSource() : dispatcherSource;
92
+ } catch (error) {
93
+ return Promise.reject(error);
94
+ }
95
+
96
+ if (dispatcher && typeof dispatcher.then === "function") {
97
+ return dispatcher.then((resolved) => applyNodeDispatcher(fetchImpl, input, init, resolved));
98
+ }
99
+ if (!dispatcher || typeof dispatcher.dispatch !== "function") {
100
+ return Promise.reject(new Error("Pi insecure TLS requires an injected Undici dispatcher"));
101
+ }
102
+ return fetchImpl(input, { ...init, dispatcher });
103
+ }
104
+
105
+ function getUndiciDispatcherConstructor(target) {
106
+ return undiciGlobalDispatchers
107
+ .map((symbol) => target[symbol]?.constructor)
108
+ .find((constructor) => typeof constructor === "function");
109
+ }
110
+
111
+ function initializeUndiciGlobalDispatcher(target) {
112
+ if (typeof target.fetch !== "function") return;
113
+ try {
114
+ void Promise.resolve(target.fetch("data:,")).catch(() => {});
115
+ } catch {
116
+ }
117
+ }
118
+
119
+ export function createNodeInsecureDispatcher(target = globalThis) {
120
+ let Dispatcher = getUndiciDispatcherConstructor(target);
121
+ if (typeof Dispatcher !== "function") {
122
+ initializeUndiciGlobalDispatcher(target);
123
+ Dispatcher = getUndiciDispatcherConstructor(target);
124
+ }
125
+ if (typeof Dispatcher !== "function") {
126
+ throw new Error("Pi insecure TLS requires the active Undici dispatcher");
127
+ }
128
+ return new Dispatcher({
129
+ allowH2: false,
130
+ connect: { rejectUnauthorized: false },
131
+ requestTls: { rejectUnauthorized: false }
132
+ });
133
+ }
134
+
135
+ export function createProviderTlsFetch({
136
+ fetchImpl = globalThis.fetch,
137
+ runtime = typeof globalThis.Bun === "undefined" ? "node" : "bun",
138
+ insecureDispatcher
139
+ } = {}) {
140
+ if (typeof fetchImpl !== "function") {
141
+ throw new TypeError("A fetch implementation is required");
142
+ }
143
+
144
+ return function providerTlsFetch(input, init) {
145
+ const marked = getMarkedHeaders(input, init);
146
+ if (!marked) return fetchImpl(input, init);
147
+
148
+ const nextInit = { ...(init ?? {}), headers: marked.headers };
149
+ if (!marked.allowedOrigin || getRequestOrigin(input) !== marked.allowedOrigin) {
150
+ return fetchImpl(input, nextInit);
151
+ }
152
+
153
+ nextInit.redirect = "manual";
154
+ const fetchOnce = runtime === "bun"
155
+ ? (nextInput, redirectInit) => {
156
+ const tls = redirectInit.tls && typeof redirectInit.tls === "object" ? redirectInit.tls : {};
157
+ return fetchImpl(nextInput, {
158
+ ...redirectInit,
159
+ tls: { ...tls, rejectUnauthorized: false }
160
+ });
161
+ }
162
+ : (nextInput, redirectInit) => applyNodeDispatcher(
163
+ fetchImpl,
164
+ nextInput,
165
+ redirectInit,
166
+ insecureDispatcher
167
+ );
168
+
169
+ return fetchWithSameOriginRedirects({
170
+ fetchOnce,
171
+ input,
172
+ init: nextInit,
173
+ allowedOrigin: marked.allowedOrigin
174
+ });
175
+ };
176
+ }
177
+
178
+ export function installProviderTlsFetch({ target = globalThis, ...options } = {}) {
179
+ if (target.fetch?.[installedFetchMarker]) return target.fetch;
180
+
181
+ const wrapped = createProviderTlsFetch({
182
+ ...options,
183
+ fetchImpl: target.fetch.bind(target)
184
+ });
185
+ Object.defineProperty(wrapped, installedFetchMarker, { value: true });
186
+ target.fetch = wrapped;
187
+ return wrapped;
188
+ }
package/lib/cli.mjs CHANGED
@@ -1,11 +1,13 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
- import { cp, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
2
+ import { chmod, cp, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import * as p from "@clack/prompts";
8
8
  import c from "picocolors";
9
+ import lockfile from "proper-lockfile";
10
+ import { piInsecureTlsHeader } from "../extensions/pi-gpt-responses-compat/tls-fetch.mjs";
9
11
  import {
10
12
  assertInteractiveMenuSupported,
11
13
  assertNotCancelled,
@@ -35,12 +37,23 @@ const codexTemplateRoot = join(packageRoot, "lib", "templates", "codex");
35
37
  const codexTemplateConfigPath = join(codexTemplateRoot, "config-base.toml");
36
38
  const codexTemplateAgentsPath = join(codexTemplateRoot, "agents");
37
39
  const piAgentDir = join(home, ".pi", "agent");
40
+ const piAuthPath = join(piAgentDir, "auth.json");
38
41
  const piModelsPath = join(piAgentDir, "models.json");
39
42
  const piSettingsPath = join(piAgentDir, "settings.json");
40
43
  const piProviderId = "gpt";
41
- const piDefaultApi = "openai-responses";
44
+ const piDefaultApi = "openai-completions";
42
45
  const piDefaultBaseUrl = "https://api.openai.com/v1";
43
46
  const piDefaultModel = "gpt-5.5";
47
+ const piAuthLockOptions = {
48
+ stale: 30000,
49
+ retries: {
50
+ retries: 10,
51
+ factor: 2,
52
+ minTimeout: 100,
53
+ maxTimeout: 10000,
54
+ randomize: true
55
+ }
56
+ };
44
57
  const installBackupStamp = Date.now();
45
58
  const createdBackupPaths = new Set();
46
59
  const augmentContextEnginePermission = "mcp__augment-context-engine";
@@ -268,6 +281,18 @@ async function backupExistingPath(targetPath) {
268
281
  return backupPath;
269
282
  }
270
283
 
284
+ async function backupPrivateFile(targetPath, content) {
285
+ if (createdBackupPaths.has(targetPath) || !(await pathExists(targetPath))) {
286
+ return null;
287
+ }
288
+
289
+ const backupPath = await createBackupPath(targetPath);
290
+ await writeFile(backupPath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
291
+ createdBackupPaths.add(targetPath);
292
+ p.log.message(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
293
+ return backupPath;
294
+ }
295
+
271
296
  async function backupIfNeeded(targetPath) {
272
297
  if (!(await pathExists(targetPath))) {
273
298
  return null;
@@ -335,6 +360,18 @@ async function removeIfNotDirectory(path) {
335
360
  await rm(path, { recursive: true, force: true });
336
361
  }
337
362
 
363
+ async function ensureManagedContainerDirectory(targetPath, sourcePath) {
364
+ if (await pathExists(targetPath)) {
365
+ const targetStat = await lstat(targetPath);
366
+ if (targetStat.isSymbolicLink() && await pathsReferToSameEntry(targetPath, sourcePath)) {
367
+ await unlink(targetPath);
368
+ }
369
+ }
370
+
371
+ await removeIfNotDirectory(targetPath);
372
+ await mkdir(targetPath, { recursive: true });
373
+ }
374
+
338
375
  async function replaceManagedEntry(source, target, entry) {
339
376
  if (await pathsReferToSameEntry(source, target)) {
340
377
  return;
@@ -510,7 +547,9 @@ async function syncManagedSubtree(sourcePath, targetPath, managedRoot, filter) {
510
547
  if (!sourceStat.isDirectory()) {
511
548
  if (await pathExists(targetPath)) {
512
549
  const targetStat = await lstat(targetPath);
513
- if (targetStat.isDirectory()) {
550
+ if (targetStat.isSymbolicLink()) {
551
+ await unlink(targetPath);
552
+ } else if (targetStat.isDirectory()) {
514
553
  await rm(targetPath, { recursive: true, force: true });
515
554
  }
516
555
  }
@@ -836,10 +875,8 @@ function isWithinManagedRoot(targetPath, managedSourceRoot) {
836
875
  async function linkClaude(agentsDir, previousLinkedTargets) {
837
876
  const claudeDir = join(home, ".claude");
838
877
  await mkdir(claudeDir, { recursive: true });
839
- await removeIfNotDirectory(join(claudeDir, "commands"));
840
- await removeIfNotDirectory(join(claudeDir, "skills"));
841
- await mkdir(join(claudeDir, "commands"), { recursive: true });
842
- await mkdir(join(claudeDir, "skills"), { recursive: true });
878
+ await ensureManagedContainerDirectory(join(claudeDir, "commands"), join(agentsDir, "commands"));
879
+ await ensureManagedContainerDirectory(join(claudeDir, "skills"), join(agentsDir, "skills"));
843
880
 
844
881
  return [
845
882
  await ensureManagedLink(
@@ -877,10 +914,8 @@ async function linkCodex(agentsDir, previousLinkedTargets) {
877
914
  const results = [];
878
915
  const codexDir = join(home, ".codex");
879
916
  await mkdir(codexDir, { recursive: true });
880
- await removeIfNotDirectory(join(codexDir, "skills"));
881
- await removeIfNotDirectory(join(codexDir, "prompts"));
882
- await mkdir(join(codexDir, "skills"), { recursive: true });
883
- await mkdir(join(codexDir, "prompts"), { recursive: true });
917
+ await ensureManagedContainerDirectory(join(codexDir, "skills"), join(agentsDir, "skills"));
918
+ await ensureManagedContainerDirectory(join(codexDir, "prompts"), join(agentsDir, "commands"));
884
919
 
885
920
  results.push(
886
921
  await ensureManagedLink(
@@ -918,12 +953,9 @@ async function linkCodex(agentsDir, previousLinkedTargets) {
918
953
  async function linkPi(agentsDir, previousLinkedTargets) {
919
954
  const results = [];
920
955
  await mkdir(piAgentDir, { recursive: true });
921
- await removeIfNotDirectory(join(piAgentDir, "skills"));
922
- await removeIfNotDirectory(join(piAgentDir, "prompts"));
923
- await removeIfNotDirectory(join(piAgentDir, "extensions"));
924
- await mkdir(join(piAgentDir, "skills"), { recursive: true });
925
- await mkdir(join(piAgentDir, "prompts"), { recursive: true });
926
- await mkdir(join(piAgentDir, "extensions"), { recursive: true });
956
+ await ensureManagedContainerDirectory(join(piAgentDir, "skills"), join(agentsDir, "skills"));
957
+ await ensureManagedContainerDirectory(join(piAgentDir, "prompts"), join(agentsDir, "commands"));
958
+ await ensureManagedContainerDirectory(join(piAgentDir, "extensions"), join(agentsDir, "extensions"));
927
959
 
928
960
  results.push(
929
961
  await ensureManagedLink(
@@ -1070,6 +1102,42 @@ async function writeJsonFileWithBackup(path, data) {
1070
1102
  await writeJsonFileSafe(path, data);
1071
1103
  }
1072
1104
 
1105
+ async function ensurePrivateJsonFile(path) {
1106
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
1107
+ try {
1108
+ await writeFile(path, "{}\n", { encoding: "utf8", flag: "wx", mode: 0o600 });
1109
+ return true;
1110
+ } catch (error) {
1111
+ if (error?.code !== "EEXIST") {
1112
+ throw error;
1113
+ }
1114
+ return false;
1115
+ }
1116
+ }
1117
+
1118
+ async function updatePiAuthFile(path, apiKey) {
1119
+ const created = await ensurePrivateJsonFile(path);
1120
+ const release = await lockfile.lock(path, piAuthLockOptions);
1121
+ try {
1122
+ await chmod(path, 0o600);
1123
+ const content = await readFile(path, "utf8");
1124
+ const auth = JSON.parse(content);
1125
+ if (!auth || typeof auth !== "object" || Array.isArray(auth)) {
1126
+ throw new TypeError("Pi auth.json must contain a JSON object");
1127
+ }
1128
+ if (!created) {
1129
+ await backupPrivateFile(path, content);
1130
+ }
1131
+ await writeFile(path, `${JSON.stringify(buildPiAuthConfig(auth, apiKey), null, 2)}\n`, {
1132
+ encoding: "utf8",
1133
+ mode: 0o600
1134
+ });
1135
+ await chmod(path, 0o600);
1136
+ } finally {
1137
+ await release();
1138
+ }
1139
+ }
1140
+
1073
1141
  function stripJsonComments(content) {
1074
1142
  let output = "";
1075
1143
  let inString = false;
@@ -1186,15 +1254,66 @@ function parsePiModelIds(value) {
1186
1254
  .filter(Boolean))];
1187
1255
  }
1188
1256
 
1189
- function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}) {
1257
+ function getOpenAiUrlPathname(value) {
1258
+ const input = String(value || "").trim();
1259
+ try {
1260
+ return new URL(input).pathname.replace(/\/+$/u, "");
1261
+ } catch {
1262
+ return input.split(/[?#]/u)[0].replace(/\/+$/u, "");
1263
+ }
1264
+ }
1265
+
1266
+ function inferPiApiFromBaseUrl(value) {
1267
+ const pathname = getOpenAiUrlPathname(value);
1268
+ if (pathname.endsWith("/v1/chat/completions")) {
1269
+ return "openai-completions";
1270
+ }
1271
+ if (pathname.endsWith("/v1/responses")) {
1272
+ return "openai-responses";
1273
+ }
1274
+ return null;
1275
+ }
1276
+
1277
+ function normalizeOpenAiBaseUrl(value) {
1278
+ const input = String(value || "").trim();
1279
+ try {
1280
+ const url = new URL(input);
1281
+ let pathname = url.pathname.replace(/\/+$/u, "")
1282
+ .replace(/\/v1\/(?:chat\/completions|responses)$/u, "/v1");
1283
+ if (!pathname.endsWith("/v1")) {
1284
+ pathname = `${pathname}/v1`;
1285
+ }
1286
+ url.pathname = pathname;
1287
+ return url.toString();
1288
+ } catch {
1289
+ const baseUrl = input.replace(/\/+$/u, "")
1290
+ .replace(/\/v1\/(?:chat\/completions|responses)$/u, "/v1");
1291
+ return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
1292
+ }
1293
+ }
1294
+
1295
+ function getPiApiPromptOptions() {
1296
+ return [
1297
+ { value: "openai-completions", label: "OpenAI Chat Completions(推荐)" },
1298
+ { value: "openai-responses", label: "OpenAI Responses API" }
1299
+ ];
1300
+ }
1301
+
1302
+ function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}, auth = {}) {
1190
1303
  const provider = modelsConfig.providers?.[piProviderId] && typeof modelsConfig.providers[piProviderId] === "object"
1191
1304
  ? modelsConfig.providers[piProviderId]
1192
1305
  : {};
1306
+ const credential = auth[piProviderId] && typeof auth[piProviderId] === "object"
1307
+ ? auth[piProviderId]
1308
+ : {};
1309
+ const authApiKey = credential.type === "api_key" && typeof credential.key === "string"
1310
+ ? credential.key
1311
+ : "";
1193
1312
  const models = Array.isArray(provider.models) ? provider.models.filter((model) => model?.id) : [];
1194
1313
  return {
1195
1314
  baseUrl: provider.baseUrl || piDefaultBaseUrl,
1196
1315
  api: provider.api || piDefaultApi,
1197
- apiKey: provider.apiKey || "",
1316
+ apiKey: authApiKey || provider.apiKey || "",
1198
1317
  modelIds: models.map((model) => model.id),
1199
1318
  defaultModel: settings.defaultProvider === piProviderId && settings.defaultModel
1200
1319
  ? settings.defaultModel
@@ -1214,30 +1333,67 @@ function buildPiModelConfig(modelId, existingModel = {}) {
1214
1333
  };
1215
1334
  }
1216
1335
 
1217
- function buildPiModelsConfig(modelsConfig = {}, { baseUrl, api, apiKey, modelIds }) {
1336
+ function hasPiInsecureTlsSetting(modelsConfig = {}) {
1337
+ const headers = modelsConfig.providers?.[piProviderId]?.headers;
1338
+ return headers && typeof headers === "object"
1339
+ ? Object.keys(headers).some((key) => key.toLowerCase() === piInsecureTlsHeader)
1340
+ : false;
1341
+ }
1342
+
1343
+ function buildPiModelsConfig(modelsConfig = {}, { baseUrl, api, apiKey, modelIds, insecureTls = false }) {
1218
1344
  const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
1219
1345
  const currentProvider = providers[piProviderId] && typeof providers[piProviderId] === "object" ? providers[piProviderId] : {};
1346
+ const headers = currentProvider.headers && typeof currentProvider.headers === "object"
1347
+ ? { ...currentProvider.headers }
1348
+ : {};
1349
+ for (const key of Object.keys(headers)) {
1350
+ if (key.toLowerCase() === piInsecureTlsHeader) {
1351
+ delete headers[key];
1352
+ }
1353
+ }
1354
+ if (insecureTls) {
1355
+ headers[piInsecureTlsHeader] = new URL(baseUrl).origin;
1356
+ }
1220
1357
  const existingModels = new Map(
1221
1358
  (Array.isArray(currentProvider.models) ? currentProvider.models : [])
1222
1359
  .filter((model) => model?.id)
1223
1360
  .map((model) => [model.id, model])
1224
1361
  );
1225
1362
 
1363
+ const provider = {
1364
+ ...currentProvider,
1365
+ baseUrl,
1366
+ api,
1367
+ apiKey,
1368
+ compat: {
1369
+ ...(currentProvider.compat && typeof currentProvider.compat === "object" ? currentProvider.compat : {}),
1370
+ supportsDeveloperRole: false
1371
+ },
1372
+ models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
1373
+ };
1374
+ if (Object.keys(headers).length) {
1375
+ provider.headers = headers;
1376
+ } else {
1377
+ delete provider.headers;
1378
+ }
1379
+
1226
1380
  return {
1227
1381
  ...modelsConfig,
1228
1382
  providers: {
1229
1383
  ...providers,
1230
- [piProviderId]: {
1231
- ...currentProvider,
1232
- baseUrl,
1233
- api,
1234
- apiKey,
1235
- compat: {
1236
- ...(currentProvider.compat && typeof currentProvider.compat === "object" ? currentProvider.compat : {}),
1237
- supportsDeveloperRole: false
1238
- },
1239
- models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
1240
- }
1384
+ [piProviderId]: provider
1385
+ }
1386
+ };
1387
+ }
1388
+
1389
+ function buildPiAuthConfig(auth = {}, apiKey) {
1390
+ const credential = auth[piProviderId] && typeof auth[piProviderId] === "object" ? auth[piProviderId] : {};
1391
+ return {
1392
+ ...auth,
1393
+ [piProviderId]: {
1394
+ ...credential,
1395
+ type: "api_key",
1396
+ key: apiKey
1241
1397
  }
1242
1398
  };
1243
1399
  }
@@ -1535,6 +1691,16 @@ function mergeClaudeSettingsWithDefaults(settings, { augmentContextEngine = fals
1535
1691
  };
1536
1692
  }
1537
1693
 
1694
+ function applyClaudeInsecureTlsSetting(env = {}, enabled = false) {
1695
+ const nextEnv = { ...env };
1696
+ if (enabled) {
1697
+ nextEnv.NODE_TLS_REJECT_UNAUTHORIZED = "0";
1698
+ } else if (nextEnv.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
1699
+ delete nextEnv.NODE_TLS_REJECT_UNAUTHORIZED;
1700
+ }
1701
+ return nextEnv;
1702
+ }
1703
+
1538
1704
  function getPreviousManagedClaudePermissions(previousMetadata = {}) {
1539
1705
  return Array.isArray(previousMetadata.managedClaudePermissions)
1540
1706
  ? previousMetadata.managedClaudePermissions.filter((value) => typeof value === "string")
@@ -1618,7 +1784,8 @@ function getExistingClaudeApiConfig(settings) {
1618
1784
  baseUrl: env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1619
1785
  authType: env.ANTHROPIC_AUTH_TOKEN ? "auth_token" : "api_key",
1620
1786
  key: env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || "",
1621
- model: claudeModelEnvKeys.map((field) => env[field]).find(Boolean) || ""
1787
+ model: claudeModelEnvKeys.map((field) => env[field]).find(Boolean) || "",
1788
+ insecureTls: env.NODE_TLS_REJECT_UNAUTHORIZED === "0"
1622
1789
  };
1623
1790
  }
1624
1791
 
@@ -1674,6 +1841,11 @@ async function configureClaudeApi() {
1674
1841
  assertNotCancelled(key);
1675
1842
  const finalKey = resolvePasswordValue(key, existing.key);
1676
1843
 
1844
+ const insecureTls = await confirmOrCancel({
1845
+ message: "是否跳过 Claude Code TLS 证书校验?仅证书无法修复时启用(会放宽该进程全部 HTTPS 请求)",
1846
+ initialValue: existing.insecureTls
1847
+ });
1848
+
1677
1849
  const model = await p.text({
1678
1850
  message: "Claude Code 模型",
1679
1851
  defaultValue: existing.model || undefined,
@@ -1682,6 +1854,7 @@ async function configureClaudeApi() {
1682
1854
  assertNotCancelled(model);
1683
1855
 
1684
1856
  const nextSettings = mergeClaudeSettingsWithDefaults(settings);
1857
+ nextSettings.env = applyClaudeInsecureTlsSetting(nextSettings.env, insecureTls);
1685
1858
  nextSettings.env.ANTHROPIC_BASE_URL = baseUrl;
1686
1859
 
1687
1860
  if (authType === "auth_token") {
@@ -2211,25 +2384,30 @@ async function ensurePiResourcesLinked(agentsDir) {
2211
2384
  }
2212
2385
 
2213
2386
  async function configurePiApi(agentsDir) {
2387
+ const auth = await readJsonFileSafe(piAuthPath, {});
2214
2388
  const modelsConfig = await readJsoncFileSafe(piModelsPath, {});
2215
2389
  const settings = await readJsonFileSafe(piSettingsPath, {});
2216
- const existing = resolveExistingPiApiConfig(modelsConfig, settings);
2390
+ const existing = resolveExistingPiApiConfig(modelsConfig, settings, auth);
2217
2391
 
2218
- const baseUrl = await p.text({
2392
+ const baseUrlInput = await p.text({
2219
2393
  message: "Pi gpt Base URL",
2220
2394
  defaultValue: existing.baseUrl,
2221
2395
  validate: required()
2222
2396
  });
2223
- assertNotCancelled(baseUrl);
2397
+ assertNotCancelled(baseUrlInput);
2398
+ const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
2399
+ const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
2400
+
2401
+ const insecureTls = await confirmOrCancel({
2402
+ message: "是否仅为 Pi gpt 中转请求跳过 TLS 证书校验?仅证书无法修复时启用",
2403
+ initialValue: hasPiInsecureTlsSetting(modelsConfig)
2404
+ });
2224
2405
 
2225
- const piApiOptions = ["openai-responses", "openai-completions"];
2226
- const api = await selectOrCancel({
2406
+ const piApiOptions = getPiApiPromptOptions();
2407
+ const api = inferredApi || await selectOrCancel({
2227
2408
  message: "Pi gpt API 类型",
2228
- options: [
2229
- { value: "openai-responses", label: "OpenAI Responses API" },
2230
- { value: "openai-completions", label: "OpenAI Chat Completions" }
2231
- ],
2232
- initialValue: piApiOptions.includes(existing.api) ? existing.api : piDefaultApi
2409
+ options: piApiOptions,
2410
+ initialValue: piApiOptions.some((option) => option.value === existing.api) ? existing.api : piDefaultApi
2233
2411
  });
2234
2412
 
2235
2413
  const apiKey = await p.password({
@@ -2258,16 +2436,19 @@ async function configurePiApi(agentsDir) {
2258
2436
 
2259
2437
  const finalDefaultModel = String(defaultModel).trim();
2260
2438
  await ensurePiResourcesLinked(agentsDir);
2439
+ await updatePiAuthFile(piAuthPath, finalApiKey);
2261
2440
  await writeJsonFileWithBackup(piModelsPath, buildPiModelsConfig(modelsConfig, {
2262
2441
  baseUrl,
2263
2442
  api,
2264
2443
  apiKey: finalApiKey,
2265
- modelIds
2444
+ modelIds,
2445
+ insecureTls
2266
2446
  }));
2267
2447
  await writeJsonFileWithBackup(piSettingsPath, buildPiSettingsConfig(settings, finalDefaultModel));
2268
2448
 
2269
2449
  p.log.step(`已更新 ${pathToLabel(piModelsPath)} (${piProviderId}, ${baseUrl})`);
2270
2450
  p.log.step(`已更新 ${pathToLabel(piSettingsPath)} (默认模型: ${finalDefaultModel})`);
2451
+ p.log.step(`已更新 ${pathToLabel(piAuthPath)} (${maskSecret(finalApiKey)})`);
2271
2452
  p.log.step(`已链接 Pi 扩展到 ${pathToLabel(join(piAgentDir, "extensions"))}`);
2272
2453
  }
2273
2454
 
@@ -2275,12 +2456,13 @@ async function configureCodexApi() {
2275
2456
  const existing = await getExistingCodexApiConfig();
2276
2457
  const providerId = existing.providerId || "abelworkflow";
2277
2458
  const providerName = existing.providerName || providerId;
2278
- const baseUrl = await p.text({
2459
+ const baseUrlInput = await p.text({
2279
2460
  message: "Codex Base URL",
2280
2461
  defaultValue: existing.baseUrl,
2281
2462
  validate: required()
2282
2463
  });
2283
- assertNotCancelled(baseUrl);
2464
+ assertNotCancelled(baseUrlInput);
2465
+ const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
2284
2466
 
2285
2467
  const apiKey = await p.password({
2286
2468
  message: "Codex 第三方 API Key(输入 - 清除)",
@@ -2315,6 +2497,9 @@ async function configureCodexApi() {
2315
2497
 
2316
2498
  p.log.step(`已更新 ${pathToLabel(codexConfigPath)} (${providerId}, ${baseUrl})`);
2317
2499
  p.log.step(`已更新 ${pathToLabel(codexAuthPath)} (${maskSecret(finalApiKey)})`);
2500
+ if (String(baseUrl).startsWith("https://") && !process.env.CODEX_CA_CERTIFICATE && !process.env.SSL_CERT_FILE) {
2501
+ p.log.message("自签名证书需在启动 Codex 前设置 CODEX_CA_CERTIFICATE=/absolute/path/relay-ca.pem。");
2502
+ }
2318
2503
  if (shouldDeploySubagents) {
2319
2504
  const deployed = await deployBundledCodexAgents();
2320
2505
  p.log.step(`已部署 ${deployed.length} 个 Codex subagents 到 ${pathToLabel(join(home, ".codex", "agents"))}`);
@@ -2354,7 +2539,8 @@ function buildCodexConfigContent(currentContent, {
2354
2539
  base_url: baseUrl,
2355
2540
  wire_api: "responses",
2356
2541
  temp_env_key: envKey,
2357
- requires_openai_auth: true
2542
+ requires_openai_auth: true,
2543
+ supports_websockets: true
2358
2544
  });
2359
2545
  return `${content.trim()}${lineEnding}`;
2360
2546
  }
@@ -2798,10 +2984,12 @@ async function main() {
2798
2984
  }
2799
2985
 
2800
2986
  export {
2987
+ applyClaudeInsecureTlsSetting,
2801
2988
  applyClaudePermissionFeature,
2802
2989
  buildCliToolInstallCommand,
2803
2990
  buildDefaultClaudeSettings,
2804
2991
  buildCodexConfigContent,
2992
+ buildPiAuthConfig,
2805
2993
  buildPiModelsConfig,
2806
2994
  buildPiSettingsConfig,
2807
2995
  chooseCliInstallPackageManager,
@@ -2810,9 +2998,12 @@ export {
2810
2998
  getRunCommandSpawnOptions,
2811
2999
  hasPromptEnhancerApiConfig,
2812
3000
  inferPackageManagerFromCommandPath,
3001
+ getPiApiPromptOptions,
3002
+ inferPiApiFromBaseUrl,
2813
3003
  main,
2814
3004
  mergeCodexAuthData,
2815
3005
  mergeClaudeSettingsWithDefaults,
3006
+ normalizeOpenAiBaseUrl,
2816
3007
  renderManagedWorkflowContent,
2817
3008
  resolveAugmentContextEngineFeature,
2818
3009
  resolvePromptEnhancerMode,
@@ -2820,5 +3011,6 @@ export {
2820
3011
  parsePiModelIds,
2821
3012
  resolveExistingPiApiConfig,
2822
3013
  stripJsonComments,
3014
+ updatePiAuthFile,
2823
3015
  updateTomlSectionFields
2824
3016
  };
@@ -12,7 +12,7 @@ pre-implementation planning, post-implementation review, or bounded
12
12
  module-level implementation.
13
13
  """
14
14
  nickname_candidates = ["Relay", "Pivot", "Anchor"]
15
- model = "gpt-5.5"
15
+ model = "gpt-5.6-sol"
16
16
  model_reasoning_effort = "high"
17
17
  sandbox_mode = "workspace-write"
18
18
 
@@ -11,7 +11,7 @@ Do NOT dispatch for review after implementation — use reviewer instead.
11
11
  Do NOT dispatch if the affected files are already known and confirmed.
12
12
  """
13
13
  nickname_candidates = ["Atlas", "Trace", "Scout"]
14
- model = "gpt-5.5"
14
+ model = "gpt-5.6-sol"
15
15
  model_reasoning_effort = "high"
16
16
  sandbox_mode = "read-only"
17
17
 
@@ -14,7 +14,7 @@ exceed execution value.
14
14
  Do NOT dispatch after implementation is complete — use reviewer instead.
15
15
  """
16
16
  nickname_candidates = ["Blueprint", "Compass", "Architect"]
17
- model = "gpt-5.5"
17
+ model = "gpt-5.6-sol"
18
18
  model_reasoning_effort = "high"
19
19
  sandbox_mode = "read-only"
20
20
 
@@ -11,7 +11,7 @@ Do NOT dispatch for codebase mapping or exploration — use explorer instead.
11
11
  Do NOT dispatch before implementation is complete.
12
12
  """
13
13
  nickname_candidates = ["Delta", "Echo", "Sigma"]
14
- model = "gpt-5.5"
14
+ model = "gpt-5.6-sol"
15
15
  model_reasoning_effort = "xhigh"
16
16
  sandbox_mode = "read-only"
17
17
 
@@ -10,7 +10,7 @@ Do NOT dispatch when the task touches global configs, shared utilities,
10
10
  public interfaces used across modules, or project scaffolding.
11
11
  """
12
12
  nickname_candidates = ["Forge", "Patch", "Builder"]
13
- model = "gpt-5.5"
13
+ model = "gpt-5.6-sol"
14
14
  model_reasoning_effort = "high"
15
15
  sandbox_mode = "workspace-write"
16
16
 
@@ -5,11 +5,9 @@ preferred_auth_method = "apikey"
5
5
  approvals_reviewer = "guardian_subagent"
6
6
  approval_policy = "on-request"
7
7
  sandbox_mode = "workspace-write"
8
- model = "gpt-5.5"
8
+ model = "gpt-5.6-sol"
9
9
  model_reasoning_effort = "high"
10
10
  network_access = true
11
- supports_websockets = true
12
- requires_openai_auth = true
13
11
  developer_instructions = """
14
12
  Act as the default orchestrator for specialized subagents.
15
13
 
@@ -95,5 +93,4 @@ job_max_runtime_seconds = 2400
95
93
  multi_agent = true
96
94
  js_repl = true
97
95
  guardian_approval = true
98
- responses_websockets_v2 = true
99
96
  shell_snapshot = true
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "abelworkflow",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Install AbelWorkflow into ~/.agents and create Claude/Codex symlinks.",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "node --test test/runtime-doc-contracts.test.mjs test/cli-contracts.test.mjs test/codex-config.test.mjs",
7
+ "test": "node --test test/runtime-doc-contracts.test.mjs test/cli-contracts.test.mjs test/codex-config.test.mjs test/pi-auth-precedence.test.mjs test/pi-auth-write.test.mjs test/pi-provider-tls.test.mjs",
8
8
  "test:contracts": "node --test test/runtime-doc-contracts.test.mjs test/cli-contracts.test.mjs"
9
9
  },
10
10
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^1.2.0",
30
- "picocolors": "^1.1.1"
30
+ "picocolors": "^1.1.1",
31
+ "proper-lockfile": "4.1.2"
31
32
  }
32
33
  }