abelworkflow 0.9.1 → 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
|
|
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-
|
|
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.
|
|
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
|
|
840
|
-
await
|
|
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
|
|
881
|
-
await
|
|
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
|
|
922
|
-
await
|
|
923
|
-
await
|
|
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
|
|
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
|
|
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
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
2226
|
-
const api = await selectOrCancel({
|
|
2406
|
+
const piApiOptions = getPiApiPromptOptions();
|
|
2407
|
+
const api = inferredApi || await selectOrCancel({
|
|
2227
2408
|
message: "Pi gpt API 类型",
|
|
2228
|
-
options:
|
|
2229
|
-
|
|
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
|
|
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(
|
|
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"))}`);
|
|
@@ -2799,10 +2984,12 @@ async function main() {
|
|
|
2799
2984
|
}
|
|
2800
2985
|
|
|
2801
2986
|
export {
|
|
2987
|
+
applyClaudeInsecureTlsSetting,
|
|
2802
2988
|
applyClaudePermissionFeature,
|
|
2803
2989
|
buildCliToolInstallCommand,
|
|
2804
2990
|
buildDefaultClaudeSettings,
|
|
2805
2991
|
buildCodexConfigContent,
|
|
2992
|
+
buildPiAuthConfig,
|
|
2806
2993
|
buildPiModelsConfig,
|
|
2807
2994
|
buildPiSettingsConfig,
|
|
2808
2995
|
chooseCliInstallPackageManager,
|
|
@@ -2811,9 +2998,12 @@ export {
|
|
|
2811
2998
|
getRunCommandSpawnOptions,
|
|
2812
2999
|
hasPromptEnhancerApiConfig,
|
|
2813
3000
|
inferPackageManagerFromCommandPath,
|
|
3001
|
+
getPiApiPromptOptions,
|
|
3002
|
+
inferPiApiFromBaseUrl,
|
|
2814
3003
|
main,
|
|
2815
3004
|
mergeCodexAuthData,
|
|
2816
3005
|
mergeClaudeSettingsWithDefaults,
|
|
3006
|
+
normalizeOpenAiBaseUrl,
|
|
2817
3007
|
renderManagedWorkflowContent,
|
|
2818
3008
|
resolveAugmentContextEngineFeature,
|
|
2819
3009
|
resolvePromptEnhancerMode,
|
|
@@ -2821,5 +3011,6 @@ export {
|
|
|
2821
3011
|
parsePiModelIds,
|
|
2822
3012
|
resolveExistingPiApiConfig,
|
|
2823
3013
|
stripJsonComments,
|
|
3014
|
+
updatePiAuthFile,
|
|
2824
3015
|
updateTomlSectionFields
|
|
2825
3016
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelworkflow",
|
|
3
|
-
"version": "0.9.
|
|
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
|
}
|