abelworkflow 1.1.1 → 1.1.3
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 +1 -1
- package/lib/cli/main.mjs +39 -48
- package/lib/cli/prompts.mjs +22 -19
- package/lib/config/dotenv.mjs +1 -9
- package/lib/config/store.mjs +0 -29
- package/lib/config/toml.mjs +10 -9
- package/lib/installer/assets.mjs +2 -6
- package/lib/installer/install.mjs +18 -43
- package/lib/installer/links.mjs +7 -18
- package/lib/installer/state.mjs +3 -22
- package/lib/providers/claude.mjs +8 -20
- package/lib/providers/codex.mjs +29 -37
- package/lib/providers/pi.mjs +98 -44
- package/lib/providers/skills.mjs +20 -27
- package/lib/templates/codex/config-base.toml +1 -2
- package/lib/utils.mjs +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ node .\bin\abelworkflow.mjs install --agents-dir "$HOME\.agents"
|
|
|
63
63
|
|
|
64
64
|
- Claude 配置默认写入 `bypassPermissions` 全权限模式(YOLO),第三方 API 仅使用 API Key 认证,不添加任何 MCP 权限。已有 permissions、deny、hooks 和 timeout 保持不变。
|
|
65
65
|
- Codex 只更新 AbelWorkflow 管理的认证字段,保留未知字段、其他 Provider 和用户 token。
|
|
66
|
-
- Pi 0.80.0 及以上版本按无会话启动时实际选中的当前有效 Provider 配置 API
|
|
66
|
+
- Pi 0.80.0 及以上版本按无会话启动时实际选中的当前有效 Provider 配置 API;首次使用且 Pi 明确没有可用模型时,引导创建 `gpt` 自定义 Provider。API Key 保存在 `~/.pi/agent/auth.json`,`models.json` 只保留模型定义;旧 models-only key 会按先写 auth、后删除旧 key 的顺序迁移。
|
|
67
67
|
- Grok 默认模型统一为 `grok-4.20-non-reasoning`。
|
|
68
68
|
- Context7 使用显式 CommonJS 入口 `context7-api.cjs`。
|
|
69
69
|
- dev-browser 发布运行时使用 Node ESM 编译产物,入口为 `node dist/scripts/start.js`,不依赖 Bun 或运行时 `npx tsx`。
|
package/lib/cli/main.mjs
CHANGED
|
@@ -126,31 +126,32 @@ ${c.bold("Default behavior:")}
|
|
|
126
126
|
`);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
async function
|
|
129
|
+
async function configureProviderWithMetadata(paths, configure) {
|
|
130
130
|
const metadata = await readInstallMetadata(paths);
|
|
131
131
|
const packageVersion = await packageVersionFor(paths, metadata);
|
|
132
|
-
const
|
|
133
|
-
managedAuthKeys: getPreviousManagedCodexAuthKeys(metadata),
|
|
134
|
-
managedCodexAgentFiles: getPreviousManagedCodexAgentFiles(metadata)
|
|
135
|
-
});
|
|
132
|
+
const overrides = await configure(metadata);
|
|
136
133
|
await writeInstallMetadata(paths, finalizeProviderInstallMetadata({
|
|
137
134
|
previousMetadata: metadata,
|
|
138
135
|
packageVersion,
|
|
139
|
-
overrides
|
|
136
|
+
overrides
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function configureCodexForPaths(paths) {
|
|
141
|
+
await configureProviderWithMetadata(paths, async (metadata) => {
|
|
142
|
+
const result = await configureCodexApi(paths, promptApi, {
|
|
143
|
+
managedAuthKeys: getPreviousManagedCodexAuthKeys(metadata),
|
|
144
|
+
managedCodexAgentFiles: getPreviousManagedCodexAgentFiles(metadata)
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
140
147
|
managedCodexAuthKeys: result.managedAuthKeys,
|
|
141
148
|
managedCodexAgentFiles: result.managedCodexAgentFiles
|
|
142
|
-
}
|
|
143
|
-
})
|
|
149
|
+
};
|
|
150
|
+
});
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
async function configureClaudeForPaths(paths) {
|
|
147
|
-
|
|
148
|
-
const packageVersion = await packageVersionFor(paths, metadata);
|
|
149
|
-
await configureClaudeApi(paths, promptApi);
|
|
150
|
-
await writeInstallMetadata(paths, finalizeProviderInstallMetadata({
|
|
151
|
-
previousMetadata: metadata,
|
|
152
|
-
packageVersion
|
|
153
|
-
}));
|
|
154
|
+
await configureProviderWithMetadata(paths, () => configureClaudeApi(paths, promptApi));
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
async function runFullInit(options) {
|
|
@@ -176,13 +177,13 @@ async function runFullInit(options) {
|
|
|
176
177
|
await installCliTool("pi", promptApi);
|
|
177
178
|
}
|
|
178
179
|
if (await confirmOrCancel({ message: "是否配置 Pi 当前有效 Provider API?", initialValue: commandExists("pi") })) {
|
|
179
|
-
await configurePiApi(options.paths,
|
|
180
|
+
await configurePiApi(options.paths, ensurePiResourcesLinkedWithReport, promptApi);
|
|
180
181
|
}
|
|
181
182
|
if (await confirmOrCancel({ message: "是否填写 grok-search 环境变量?", initialValue: false })) {
|
|
182
|
-
await configureGrokSearchEnv(options.paths, (paths) =>
|
|
183
|
+
await configureGrokSearchEnv(options.paths, (paths) => ensureSkillPresentWithReport(paths, "grok-search"), promptApi);
|
|
183
184
|
}
|
|
184
185
|
if (await confirmOrCancel({ message: "是否填写 context7-auto-research 环境变量?", initialValue: false })) {
|
|
185
|
-
await configureContext7Env(options.paths, (paths) =>
|
|
186
|
+
await configureContext7Env(options.paths, (paths) => ensureSkillPresentWithReport(paths, "context7-auto-research"), promptApi);
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
const conflictCount = uniqueReportPaths(installReport.conflicts).length;
|
|
@@ -205,35 +206,31 @@ async function runInteractiveMenu(options) {
|
|
|
205
206
|
return opt;
|
|
206
207
|
};
|
|
207
208
|
const cliToolMenus = {
|
|
208
|
-
|
|
209
|
-
tool: "pi",
|
|
209
|
+
pi: {
|
|
210
210
|
title: "Pi",
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
"pi-api": async () => configurePiApi(options.paths, ensurePiResourcesLinkedWithReport, promptApi)
|
|
214
|
-
}
|
|
211
|
+
install: () => installCliTool("pi", promptApi),
|
|
212
|
+
configure: () => configurePiApi(options.paths, ensurePiResourcesLinkedWithReport, promptApi)
|
|
215
213
|
},
|
|
216
|
-
|
|
217
|
-
tool: "codex",
|
|
214
|
+
codex: {
|
|
218
215
|
title: "Codex",
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"codex-api": async () => configureCodexForPaths(options.paths)
|
|
222
|
-
}
|
|
216
|
+
install: () => installCliTool("codex", promptApi),
|
|
217
|
+
configure: () => configureCodexForPaths(options.paths)
|
|
223
218
|
},
|
|
224
|
-
|
|
225
|
-
tool: "claude",
|
|
219
|
+
claude: {
|
|
226
220
|
title: "Claude Code",
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
"claude-api": async () => configureClaudeForPaths(options.paths)
|
|
230
|
-
}
|
|
221
|
+
install: () => installCliTool("claude", promptApi),
|
|
222
|
+
configure: () => configureClaudeForPaths(options.paths)
|
|
231
223
|
}
|
|
232
224
|
};
|
|
233
|
-
const runCliToolMenu = async (
|
|
225
|
+
const runCliToolMenu = async (tool) => {
|
|
226
|
+
const menu = cliToolMenus[tool];
|
|
227
|
+
const actions = {
|
|
228
|
+
[`${tool}-api`]: menu.configure,
|
|
229
|
+
[`${tool}-install`]: menu.install
|
|
230
|
+
};
|
|
234
231
|
while (true) {
|
|
235
232
|
const choice = await p.select({
|
|
236
|
-
message: `请选择 ${title} 操作`,
|
|
233
|
+
message: `请选择 ${menu.title} 操作`,
|
|
237
234
|
options: buildCliToolMenuDescriptors(tool).map(buildOption),
|
|
238
235
|
initialValue: `${tool}-api`
|
|
239
236
|
});
|
|
@@ -259,9 +256,9 @@ async function runInteractiveMenu(options) {
|
|
|
259
256
|
}),
|
|
260
257
|
"grok-search": async () => configureGrokSearchEnv(options.paths, (paths) => ensureSkillPresentWithReport(paths, "grok-search"), promptApi),
|
|
261
258
|
context7: async () => configureContext7Env(options.paths, (paths) => ensureSkillPresentWithReport(paths, "context7-auto-research"), promptApi),
|
|
262
|
-
"pi-cli": async () => runCliToolMenu(
|
|
263
|
-
"codex-cli": async () => runCliToolMenu(
|
|
264
|
-
"claude-cli": async () => runCliToolMenu(
|
|
259
|
+
"pi-cli": async () => runCliToolMenu("pi"),
|
|
260
|
+
"codex-cli": async () => runCliToolMenu("codex"),
|
|
261
|
+
"claude-cli": async () => runCliToolMenu("claude")
|
|
265
262
|
};
|
|
266
263
|
|
|
267
264
|
while (true) {
|
|
@@ -376,10 +373,4 @@ async function main(argv, runtime = {}) {
|
|
|
376
373
|
}
|
|
377
374
|
}
|
|
378
375
|
|
|
379
|
-
export {
|
|
380
|
-
main,
|
|
381
|
-
presentInstallReport,
|
|
382
|
-
printHelp,
|
|
383
|
-
runFullInit,
|
|
384
|
-
runInteractiveMenu
|
|
385
|
-
};
|
|
376
|
+
export { main, presentInstallReport };
|
package/lib/cli/prompts.mjs
CHANGED
|
@@ -11,22 +11,10 @@ const interactiveMenuDescriptors = [
|
|
|
11
11
|
{ value: "exit", label: "退出", group: "exit" }
|
|
12
12
|
];
|
|
13
13
|
const interactiveMenuDefaultValue = "full-init";
|
|
14
|
-
const
|
|
15
|
-
pi:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{ value: "back", label: "返回上一级" }
|
|
19
|
-
],
|
|
20
|
-
codex: [
|
|
21
|
-
{ value: "codex-api", label: "配置 Codex API" },
|
|
22
|
-
{ value: "codex-install", label: "安装/更新 Codex" },
|
|
23
|
-
{ value: "back", label: "返回上一级" }
|
|
24
|
-
],
|
|
25
|
-
claude: [
|
|
26
|
-
{ value: "claude-api", label: "配置 Claude Code API" },
|
|
27
|
-
{ value: "claude-install", label: "安装/更新 Claude Code" },
|
|
28
|
-
{ value: "back", label: "返回上一级" }
|
|
29
|
-
]
|
|
14
|
+
const cliToolLabels = {
|
|
15
|
+
pi: "Pi",
|
|
16
|
+
codex: "Codex",
|
|
17
|
+
claude: "Claude Code"
|
|
30
18
|
};
|
|
31
19
|
class CancelledError extends Error {
|
|
32
20
|
constructor(message = "用户取消") {
|
|
@@ -36,9 +24,13 @@ class CancelledError extends Error {
|
|
|
36
24
|
}
|
|
37
25
|
|
|
38
26
|
function buildCliToolMenuDescriptors(tool) {
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
return
|
|
27
|
+
const label = cliToolLabels[tool];
|
|
28
|
+
if (!label) throw new Error(`Unknown CLI tool: ${tool}`);
|
|
29
|
+
return [
|
|
30
|
+
{ value: `${tool}-api`, label: `配置 ${label} API` },
|
|
31
|
+
{ value: `${tool}-install`, label: `安装/更新 ${label}` },
|
|
32
|
+
{ value: "back", label: "返回上一级" }
|
|
33
|
+
];
|
|
42
34
|
}
|
|
43
35
|
|
|
44
36
|
function required(message = "此项不能为空") {
|
|
@@ -79,6 +71,16 @@ function assertNotCancelled(value) {
|
|
|
79
71
|
if (p.isCancel(value)) throw new CancelledError();
|
|
80
72
|
}
|
|
81
73
|
|
|
74
|
+
async function passwordOrExisting({ message, existingValue, requiredMessage = "API Key 不能为空" }) {
|
|
75
|
+
const input = await p.password(passwordPromptOptions(
|
|
76
|
+
message,
|
|
77
|
+
existingValue,
|
|
78
|
+
requiredMessage ? requiredUnlessExisting(existingValue, requiredMessage) : undefined
|
|
79
|
+
));
|
|
80
|
+
assertNotCancelled(input);
|
|
81
|
+
return resolvePasswordValue(input, existingValue);
|
|
82
|
+
}
|
|
83
|
+
|
|
82
84
|
async function confirmOrCancel({ message, initialValue = false }) {
|
|
83
85
|
const value = await p.confirm({ message, initialValue, active: "是", inactive: "否" });
|
|
84
86
|
assertNotCancelled(value);
|
|
@@ -98,6 +100,7 @@ export {
|
|
|
98
100
|
confirmOrCancel,
|
|
99
101
|
interactiveMenuDefaultValue,
|
|
100
102
|
interactiveMenuDescriptors,
|
|
103
|
+
passwordOrExisting,
|
|
101
104
|
passwordPromptOptions,
|
|
102
105
|
required,
|
|
103
106
|
requiredUnlessExisting,
|
package/lib/config/dotenv.mjs
CHANGED
|
@@ -34,14 +34,6 @@ function quoteEnvValue(value) {
|
|
|
34
34
|
throw new Error("Environment value cannot be represented without changing its meaning");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function renderDotenv(values) {
|
|
38
|
-
const lines = Object.entries(values)
|
|
39
|
-
.filter(([, value]) => value !== undefined && value !== null && value !== "")
|
|
40
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
41
|
-
.map(([key, value]) => `${key}=${quoteEnvValue(String(value))}`);
|
|
42
|
-
return lines.length ? `${lines.join("\n")}\n` : "";
|
|
43
|
-
}
|
|
44
|
-
|
|
45
37
|
function getDotenvLines(content) {
|
|
46
38
|
const lines = [];
|
|
47
39
|
for (let start = 0; start < content.length;) {
|
|
@@ -99,4 +91,4 @@ function updateDotenvContent(content, updates) {
|
|
|
99
91
|
return next;
|
|
100
92
|
}
|
|
101
93
|
|
|
102
|
-
export { parseDotenv, quoteEnvValue,
|
|
94
|
+
export { parseDotenv, quoteEnvValue, updateDotenvContent };
|
package/lib/config/store.mjs
CHANGED
|
@@ -6,7 +6,6 @@ import { stripJsonComments } from "./jsonc.mjs";
|
|
|
6
6
|
|
|
7
7
|
const newBackupMarker = ".abelworkflow.bak.";
|
|
8
8
|
const newBackupSuffixPattern = /^\d+-\d+-\d{10,}$/u;
|
|
9
|
-
const chmod = fs.chmod;
|
|
10
9
|
let uniqueFileIndex = 0;
|
|
11
10
|
|
|
12
11
|
function nextUniqueSuffix() {
|
|
@@ -225,21 +224,6 @@ async function updateLockedJson(path, updater, options = {}) {
|
|
|
225
224
|
}
|
|
226
225
|
}
|
|
227
226
|
|
|
228
|
-
async function backupExistingPath(targetPath, options = {}) {
|
|
229
|
-
return copyBackup(targetPath, options);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function backupPrivateFile(targetPath, content, options = {}) {
|
|
233
|
-
if (!(await pathExists(targetPath))) return null;
|
|
234
|
-
const backupLimit = options.backupLimit ?? 3;
|
|
235
|
-
if (backupLimit === 0) return null;
|
|
236
|
-
const backupPath = await createBackupPath(targetPath);
|
|
237
|
-
await fs.writeFile(backupPath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
238
|
-
if (isPosix()) await fs.chmod(backupPath, 0o600);
|
|
239
|
-
await pruneNewBackups(targetPath, backupLimit);
|
|
240
|
-
return backupPath;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
227
|
async function backupIfNeeded(targetPath, options = {}) {
|
|
244
228
|
if (!(await pathExists(targetPath))) return null;
|
|
245
229
|
const backupLimit = options.backupLimit ?? 3;
|
|
@@ -281,19 +265,8 @@ async function updateDotenvFile(path, updates, options = {}) {
|
|
|
281
265
|
return writeText(path, updateDotenvContent(content, updates), options);
|
|
282
266
|
}
|
|
283
267
|
|
|
284
|
-
async function writeJsonFileSafe(path, data, options = {}) {
|
|
285
|
-
return writeJson(path, data, { ...options, backupLimit: options.backupLimit ?? 0 });
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async function writeJsonFileWithBackup(path, data, options = {}) {
|
|
289
|
-
return writeJson(path, data, options);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
268
|
export {
|
|
293
|
-
backupExistingPath,
|
|
294
269
|
backupIfNeeded,
|
|
295
|
-
backupPrivateFile,
|
|
296
|
-
chmod,
|
|
297
270
|
ensurePrivateJsonFile,
|
|
298
271
|
pathExists,
|
|
299
272
|
pathTargetExists,
|
|
@@ -304,7 +277,5 @@ export {
|
|
|
304
277
|
updateDotenvFile,
|
|
305
278
|
updateLockedJson,
|
|
306
279
|
writeJson,
|
|
307
|
-
writeJsonFileSafe,
|
|
308
|
-
writeJsonFileWithBackup,
|
|
309
280
|
writeText
|
|
310
281
|
};
|
package/lib/config/toml.mjs
CHANGED
|
@@ -46,6 +46,15 @@ function removeTomlSection(content, sectionName) {
|
|
|
46
46
|
return `${content.slice(0, section.start)}${content.slice(section.end)}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function removeTomlSectionField(content, sectionName, field) {
|
|
50
|
+
const document = parseTomlDocument(content);
|
|
51
|
+
const section = findTomlSection(document, sectionName);
|
|
52
|
+
const entry = section && findTomlAssignment(document.assignments, field, section);
|
|
53
|
+
return entry
|
|
54
|
+
? `${content.slice(0, entry.start)}${content.slice(entry.lineEnd)}`
|
|
55
|
+
: content;
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
function buildTomlSection(sectionName, values, lineEnding = "\n") {
|
|
50
59
|
const lines = [`[${sectionName}]`];
|
|
51
60
|
for (const [key, value] of Object.entries(values)) {
|
|
@@ -399,14 +408,6 @@ function findTomlAssignment(assignments, field, section) {
|
|
|
399
408
|
));
|
|
400
409
|
}
|
|
401
410
|
|
|
402
|
-
function splitTopLevelTomlContent(content) {
|
|
403
|
-
const { topLevelEnd } = parseTomlDocument(content);
|
|
404
|
-
return {
|
|
405
|
-
topLevel: topLevelEnd === -1 ? content : content.slice(0, topLevelEnd),
|
|
406
|
-
rest: topLevelEnd === -1 ? "" : content.slice(topLevelEnd)
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
411
|
function extractTopLevelTomlEntries(content) {
|
|
411
412
|
const document = parseTomlDocument(content);
|
|
412
413
|
return document.assignments
|
|
@@ -631,8 +632,8 @@ export {
|
|
|
631
632
|
parseTomlSection,
|
|
632
633
|
readTopLevelTomlString,
|
|
633
634
|
removeTomlSection,
|
|
635
|
+
removeTomlSectionField,
|
|
634
636
|
removeTopLevelTomlField,
|
|
635
|
-
splitTopLevelTomlContent,
|
|
636
637
|
updateTomlSectionFields,
|
|
637
638
|
updateTopLevelTomlField
|
|
638
639
|
};
|
package/lib/installer/assets.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import * as fs from "node:fs/promises";
|
|
3
2
|
import { join, relative, resolve } from "node:path";
|
|
4
3
|
import {
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
readJsonFileSafe,
|
|
9
8
|
writeText
|
|
10
9
|
} from "../config/store.mjs";
|
|
10
|
+
import { hashBytes } from "../utils.mjs";
|
|
11
11
|
import { createInstallReport, readInstallMetadata } from "./state.mjs";
|
|
12
12
|
|
|
13
13
|
const ignoredSkillPathPatterns = [
|
|
@@ -69,10 +69,6 @@ function mapGitignoreTemplate(relativePath) {
|
|
|
69
69
|
return relativePath.replace(/(^|\/)gitignore\.template$/u, "$1.gitignore");
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function hashBytes(content) {
|
|
73
|
-
return createHash("sha256").update(content).digest("hex");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
72
|
function shouldCopySkillPath(skillsRoot, sourcePath) {
|
|
77
73
|
const relativePath = normalizeRelativePath(relative(skillsRoot, sourcePath));
|
|
78
74
|
if (!relativePath) return true;
|
|
@@ -316,9 +312,9 @@ async function ensureManagedContainerDirectory(targetPath, sourcePath) {
|
|
|
316
312
|
export {
|
|
317
313
|
collectManagedAssets,
|
|
318
314
|
ensureManagedContainerDirectory,
|
|
319
|
-
hashBytes,
|
|
320
315
|
pathsReferToSameEntry,
|
|
321
316
|
readManagedFiles,
|
|
317
|
+
readPackageVersion,
|
|
322
318
|
shouldCopySkillPath,
|
|
323
319
|
syncManagedFiles
|
|
324
320
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { readManagedFiles, syncManagedFiles } from "./assets.mjs";
|
|
1
|
+
import { pathExists } from "../config/store.mjs";
|
|
2
|
+
import { createPaths } from "../paths.mjs";
|
|
3
|
+
import { readManagedFiles, readPackageVersion, syncManagedFiles } from "./assets.mjs";
|
|
5
4
|
import { linkClaude, linkCodex, linkPi } from "./links.mjs";
|
|
6
5
|
import {
|
|
7
6
|
buildInstallMetadata,
|
|
@@ -27,15 +26,6 @@ function validatedPaths(paths) {
|
|
|
27
26
|
});
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
function resolveInstallPaths(options = {}) {
|
|
31
|
-
if (options.paths) return validatedPaths(options.paths);
|
|
32
|
-
return createPaths({
|
|
33
|
-
homeDir: options.homeDir ?? defaultPaths.homeDir,
|
|
34
|
-
packageRoot: options.packageRoot ?? defaultPaths.packageRoot,
|
|
35
|
-
agentsDir: options.agentsDir ?? defaultPaths.agentsDir
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
29
|
function reportFromLinkResults(results) {
|
|
40
30
|
const report = createInstallReport();
|
|
41
31
|
for (const result of results) {
|
|
@@ -58,12 +48,17 @@ async function packageVersionFor(paths, previousMetadata) {
|
|
|
58
48
|
&& previousMetadata.packageVersion) {
|
|
59
49
|
return previousMetadata.packageVersion;
|
|
60
50
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
return readPackageVersion(paths);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function writeInstallState(paths, assetResult, linkedTargets) {
|
|
55
|
+
const metadata = buildInstallMetadata({
|
|
56
|
+
previousMetadata: assetResult.previousMetadata,
|
|
57
|
+
packageVersion: assetResult.packageVersion,
|
|
58
|
+
managedFiles: assetResult.managedFiles,
|
|
59
|
+
linkedTargets
|
|
60
|
+
});
|
|
61
|
+
await writeInstallMetadata(paths, metadata);
|
|
67
62
|
}
|
|
68
63
|
|
|
69
64
|
async function installWorkflow(options) {
|
|
@@ -94,20 +89,12 @@ async function installWorkflow(options) {
|
|
|
94
89
|
const codexResults = await linkCodex(paths, previousLinkedTargets, linkOptions);
|
|
95
90
|
const piResults = await linkPi(paths, previousLinkedTargets, linkOptions);
|
|
96
91
|
const linkResults = [...claudeResults, ...codexResults, ...piResults];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const metadata = buildInstallMetadata({
|
|
100
|
-
previousMetadata,
|
|
101
|
-
packageVersion: assetResult.packageVersion,
|
|
102
|
-
managedFiles: assetResult.managedFiles,
|
|
103
|
-
linkedTargets
|
|
104
|
-
});
|
|
105
|
-
await writeInstallMetadata(paths, metadata);
|
|
92
|
+
await writeInstallState(paths, assetResult, mergeLinkedTargets(previousLinkedTargets, linkResults));
|
|
106
93
|
return mergeInstallReports(assetResult.report, reportFromLinkResults(linkResults));
|
|
107
94
|
}
|
|
108
95
|
|
|
109
96
|
async function installManagedWorkflow(options = {}) {
|
|
110
|
-
return installWorkflow({ ...options, paths:
|
|
97
|
+
return installWorkflow({ ...options, paths: validatedPaths(options.paths) });
|
|
111
98
|
}
|
|
112
99
|
|
|
113
100
|
async function ensureSkillPresent(inputPaths, skillName) {
|
|
@@ -118,13 +105,7 @@ async function ensureSkillPresent(inputPaths, skillName) {
|
|
|
118
105
|
force: false,
|
|
119
106
|
pathPrefix: `skills/${skillName}/`
|
|
120
107
|
});
|
|
121
|
-
|
|
122
|
-
previousMetadata,
|
|
123
|
-
packageVersion: assetResult.packageVersion,
|
|
124
|
-
managedFiles: assetResult.managedFiles,
|
|
125
|
-
linkedTargets: previousMetadata.linkedTargets ?? {}
|
|
126
|
-
});
|
|
127
|
-
await writeInstallMetadata(paths, metadata);
|
|
108
|
+
await writeInstallState(paths, assetResult, previousMetadata.linkedTargets ?? {});
|
|
128
109
|
return assetResult.report;
|
|
129
110
|
}
|
|
130
111
|
|
|
@@ -134,13 +115,7 @@ async function ensurePiResourcesLinked(inputPaths) {
|
|
|
134
115
|
const assetResult = await syncManagedFiles({ paths, force: false });
|
|
135
116
|
const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
|
|
136
117
|
const piResults = await linkPi(paths, previousLinkedTargets, { force: false });
|
|
137
|
-
|
|
138
|
-
previousMetadata,
|
|
139
|
-
packageVersion: assetResult.packageVersion,
|
|
140
|
-
managedFiles: assetResult.managedFiles,
|
|
141
|
-
linkedTargets: mergeLinkedTargets(previousLinkedTargets, piResults)
|
|
142
|
-
});
|
|
143
|
-
await writeInstallMetadata(paths, metadata);
|
|
118
|
+
await writeInstallState(paths, assetResult, mergeLinkedTargets(previousLinkedTargets, piResults));
|
|
144
119
|
return mergeInstallReports(assetResult.report, reportFromLinkResults(piResults));
|
|
145
120
|
}
|
|
146
121
|
|
package/lib/installer/links.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { backupIfNeeded, pathExists, pathTargetExists } from "../config/store.mjs";
|
|
4
|
-
import { isWindows } from "../paths.mjs";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
function shouldForceFileSymlinkFailure(kind) {
|
|
8
|
-
return process.env.ABELWORKFLOW_TEST_FORCE_FILE_SYMLINK_EPERM === "1" && isWindows() && kind === "file";
|
|
9
|
-
}
|
|
4
|
+
import { containsPath, isWindows } from "../paths.mjs";
|
|
5
|
+
import { hashBytes } from "../utils.mjs";
|
|
6
|
+
import { ensureManagedContainerDirectory } from "./assets.mjs";
|
|
10
7
|
|
|
11
8
|
async function createManagedTargetState(targetPath, sourcePath, kind, mode, status) {
|
|
12
9
|
let targetHash;
|
|
@@ -16,12 +13,7 @@ async function createManagedTargetState(targetPath, sourcePath, kind, mode, stat
|
|
|
16
13
|
return { targetPath, sourcePath, kind, mode, status, ...(targetHash ? { targetHash } : {}) };
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
async function createSymlink(targetPath, sourcePath, linkType
|
|
20
|
-
if (shouldForceFileSymlinkFailure(kind)) {
|
|
21
|
-
const error = new Error("simulated EPERM");
|
|
22
|
-
error.code = "EPERM";
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
16
|
+
async function createSymlink(targetPath, sourcePath, linkType) {
|
|
25
17
|
await fs.symlink(sourcePath, targetPath, linkType);
|
|
26
18
|
}
|
|
27
19
|
|
|
@@ -150,7 +142,7 @@ async function ensureManagedLink(targetPath, sourcePath, kind, previousLinkedTar
|
|
|
150
142
|
|
|
151
143
|
const linkType = isWindows() ? (kind === "dir" ? "junction" : "file") : kind;
|
|
152
144
|
try {
|
|
153
|
-
await createSymlink(targetPath, sourcePath, linkType
|
|
145
|
+
await createSymlink(targetPath, sourcePath, linkType);
|
|
154
146
|
return createManagedTargetState(targetPath, sourcePath, kind, "symlink", "linked");
|
|
155
147
|
} catch (error) {
|
|
156
148
|
if (!shouldFallbackToManagedFile(error, kind)) throw error;
|
|
@@ -258,11 +250,8 @@ async function getPiExtensionNames(extensionsDir) {
|
|
|
258
250
|
}
|
|
259
251
|
|
|
260
252
|
function isWithinManagedRoot(targetPath, managedSourceRoot, pathOps = { isAbsolute, relative, sep }) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
&& !pathOps.isAbsolute(relativePath)
|
|
264
|
-
&& relativePath !== ".."
|
|
265
|
-
&& !relativePath.startsWith(`..${pathOps.sep}`);
|
|
253
|
+
return pathOps.relative(managedSourceRoot, targetPath) !== ""
|
|
254
|
+
&& containsPath(managedSourceRoot, targetPath, pathOps);
|
|
266
255
|
}
|
|
267
256
|
|
|
268
257
|
async function pruneManagedTargets(targetDir, managedSourceRoot, expectedNames, previousLinkedTargets, options = {}) {
|
package/lib/installer/state.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { readJsonFileSafe, writeJson } from "../config/store.mjs";
|
|
3
|
+
import { isManagedCodexAgentFileEntry } from "../utils.mjs";
|
|
3
4
|
|
|
4
5
|
function metadataPathFor(paths) {
|
|
5
6
|
return join(paths.agentsDir, paths.installMetadataName);
|
|
@@ -30,11 +31,7 @@ function getPreviousManagedCodexAuthKeys(previousMetadata = {}) {
|
|
|
30
31
|
function normalizeManagedCodexAgentFiles(value = {}) {
|
|
31
32
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
32
33
|
return sortObject(Object.fromEntries(Object.entries(value).filter(([name, hash]) => (
|
|
33
|
-
name
|
|
34
|
-
&& !name.includes("/")
|
|
35
|
-
&& !name.includes("\\")
|
|
36
|
-
&& typeof hash === "string"
|
|
37
|
-
&& /^[a-f0-9]{64}$/u.test(hash)
|
|
34
|
+
isManagedCodexAgentFileEntry(name, hash)
|
|
38
35
|
))));
|
|
39
36
|
}
|
|
40
37
|
|
|
@@ -63,7 +60,7 @@ function buildInstallMetadata({
|
|
|
63
60
|
managedCodexAgentFiles = getPreviousManagedCodexAgentFiles(previousMetadata),
|
|
64
61
|
linkedTargets = {}
|
|
65
62
|
}) {
|
|
66
|
-
|
|
63
|
+
return {
|
|
67
64
|
schemaVersion: 2,
|
|
68
65
|
packageVersion,
|
|
69
66
|
managedFiles: sortObject(managedFiles),
|
|
@@ -71,10 +68,6 @@ function buildInstallMetadata({
|
|
|
71
68
|
managedCodexAgentFiles: normalizeManagedCodexAgentFiles(managedCodexAgentFiles),
|
|
72
69
|
linkedTargets: sortObject(linkedTargets)
|
|
73
70
|
};
|
|
74
|
-
if (typeof previousMetadata.installedAt === "string") {
|
|
75
|
-
metadata.installedAt = previousMetadata.installedAt;
|
|
76
|
-
}
|
|
77
|
-
return metadata;
|
|
78
71
|
}
|
|
79
72
|
|
|
80
73
|
function finalizeProviderInstallMetadata({
|
|
@@ -100,17 +93,6 @@ function finalizeProviderInstallMetadata({
|
|
|
100
93
|
});
|
|
101
94
|
}
|
|
102
95
|
|
|
103
|
-
function linkedTargetsFromResults(results) {
|
|
104
|
-
return Object.fromEntries(results
|
|
105
|
-
.filter((result) => result.sourcePath)
|
|
106
|
-
.map((result) => [result.targetPath, {
|
|
107
|
-
sourcePath: result.sourcePath,
|
|
108
|
-
kind: result.kind,
|
|
109
|
-
mode: result.mode,
|
|
110
|
-
...(result.targetHash ? { targetHash: result.targetHash } : {})
|
|
111
|
-
}]));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
96
|
function mergeLinkedTargets(previousLinkedTargets = {}, results = []) {
|
|
115
97
|
const nextLinkedTargets = { ...previousLinkedTargets };
|
|
116
98
|
for (const result of results) {
|
|
@@ -154,7 +136,6 @@ export {
|
|
|
154
136
|
finalizeProviderInstallMetadata,
|
|
155
137
|
getPreviousManagedCodexAgentFiles,
|
|
156
138
|
getPreviousManagedCodexAuthKeys,
|
|
157
|
-
linkedTargetsFromResults,
|
|
158
139
|
mergeInstallReports,
|
|
159
140
|
mergeLinkedTargets,
|
|
160
141
|
metadataPathFor,
|
package/lib/providers/claude.mjs
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import {
|
|
3
|
-
readJsonFileSafe,
|
|
4
|
-
writeJsonFileWithBackup
|
|
5
|
-
} from "../config/store.mjs";
|
|
2
|
+
import { readJsonFileSafe, writeJson } from "../config/store.mjs";
|
|
6
3
|
import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
|
|
7
4
|
|
|
8
5
|
const claudeModelEnvKeys = [
|
|
@@ -111,18 +108,12 @@ function buildClaudeApiSettings(settings, {
|
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
async function persistClaudeConfiguration(paths, { settings, metaConfig }) {
|
|
114
|
-
await
|
|
115
|
-
await
|
|
111
|
+
await writeJson(paths.claudeSettingsPath, settings, { sensitive: true });
|
|
112
|
+
await writeJson(paths.claudeMetaConfigPath, metaConfig, { sensitive: true });
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
async function configureClaudeApi(paths = defaultPaths, promptApi) {
|
|
119
|
-
const {
|
|
120
|
-
assertNotCancelled,
|
|
121
|
-
passwordPromptOptions,
|
|
122
|
-
required,
|
|
123
|
-
requiredUnlessExisting,
|
|
124
|
-
resolvePasswordValue
|
|
125
|
-
} = promptApi;
|
|
116
|
+
const { assertNotCancelled, passwordOrExisting, required } = promptApi;
|
|
126
117
|
const settings = await readJsonFileSafe(paths.claudeSettingsPath, {}, { sensitive: true });
|
|
127
118
|
const existing = getExistingClaudeApiConfig(settings);
|
|
128
119
|
|
|
@@ -133,13 +124,10 @@ async function configureClaudeApi(paths = defaultPaths, promptApi) {
|
|
|
133
124
|
});
|
|
134
125
|
assertNotCancelled(baseUrl);
|
|
135
126
|
|
|
136
|
-
const
|
|
137
|
-
"Claude Code API Key",
|
|
138
|
-
existing.key
|
|
139
|
-
|
|
140
|
-
));
|
|
141
|
-
assertNotCancelled(key);
|
|
142
|
-
const finalKey = resolvePasswordValue(key, existing.key);
|
|
127
|
+
const finalKey = await passwordOrExisting({
|
|
128
|
+
message: "Claude Code API Key",
|
|
129
|
+
existingValue: existing.key
|
|
130
|
+
});
|
|
143
131
|
|
|
144
132
|
const model = await p.text({
|
|
145
133
|
message: "Claude Code 模型",
|
package/lib/providers/codex.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { lstat, mkdir, readFile, readdir, unlink } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import * as p from "@clack/prompts";
|
|
5
4
|
import {
|
|
6
5
|
pathExists,
|
|
7
6
|
readJsonFileSafe,
|
|
8
|
-
|
|
7
|
+
writeJson,
|
|
9
8
|
writeText
|
|
10
9
|
} from "../config/store.mjs";
|
|
11
10
|
import {
|
|
@@ -18,11 +17,13 @@ import {
|
|
|
18
17
|
parseTomlSection,
|
|
19
18
|
readTopLevelTomlString,
|
|
20
19
|
removeTomlSection,
|
|
20
|
+
removeTomlSectionField,
|
|
21
21
|
removeTopLevelTomlField,
|
|
22
22
|
updateTomlSectionFields,
|
|
23
23
|
updateTopLevelTomlField
|
|
24
24
|
} from "../config/toml.mjs";
|
|
25
25
|
import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
|
|
26
|
+
import { hashBytes, isManagedCodexAgentFileEntry } from "../utils.mjs";
|
|
26
27
|
import { normalizeOpenAiBaseUrl } from "./pi.mjs";
|
|
27
28
|
|
|
28
29
|
const CODEX_ENV_KEY = "OPENAI_API_KEY";
|
|
@@ -98,7 +99,7 @@ function mergeCodexTemplateDefaults(content, templateContent) {
|
|
|
98
99
|
const currentDeveloperInstructions = readTopLevelTomlString(nextContent, "developer_instructions");
|
|
99
100
|
const templateDeveloperInstructions = readTopLevelTomlString(templateContent, "developer_instructions");
|
|
100
101
|
if (templateDeveloperInstructions && publishedCodexDeveloperInstructionHashes.has(
|
|
101
|
-
|
|
102
|
+
hashBytes(currentDeveloperInstructions)
|
|
102
103
|
)) {
|
|
103
104
|
nextContent = updateTopLevelTomlField(
|
|
104
105
|
nextContent,
|
|
@@ -125,10 +126,6 @@ async function loadBundledCodexConfigTemplate(paths = defaultPaths) {
|
|
|
125
126
|
return readFile(paths.codexTemplateConfigPath, "utf8");
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
function sha256(content) {
|
|
129
|
-
return createHash("sha256").update(content).digest("hex");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
129
|
async function readCodexAgentTarget(path) {
|
|
133
130
|
try {
|
|
134
131
|
const targetStat = await lstat(path);
|
|
@@ -175,14 +172,6 @@ async function ensureCodexAgentContainer(homeDir) {
|
|
|
175
172
|
return targetDir;
|
|
176
173
|
}
|
|
177
174
|
|
|
178
|
-
function isCodexAgentOwnership(name, hash) {
|
|
179
|
-
return name.endsWith(".toml")
|
|
180
|
-
&& !name.includes("/")
|
|
181
|
-
&& !name.includes("\\")
|
|
182
|
-
&& typeof hash === "string"
|
|
183
|
-
&& /^[a-f0-9]{64}$/u.test(hash);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
175
|
function isPublishedCodexAgent(name, hash) {
|
|
187
176
|
return publishedCodexAgentHashes[name]?.has(hash) ?? false;
|
|
188
177
|
}
|
|
@@ -205,9 +194,9 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
|
|
|
205
194
|
const source = join(paths.codexTemplateAgentsPath, name);
|
|
206
195
|
const target = join(targetDir, name);
|
|
207
196
|
const sourceContent = await readFile(source);
|
|
208
|
-
const sourceHash =
|
|
197
|
+
const sourceHash = hashBytes(sourceContent);
|
|
209
198
|
const current = await readCodexAgentTarget(target);
|
|
210
|
-
const currentHash = current.content ?
|
|
199
|
+
const currentHash = current.content ? hashBytes(current.content) : "";
|
|
211
200
|
|
|
212
201
|
if (!current.exists) {
|
|
213
202
|
await writeText(target, sourceContent, { backupLimit: 0 });
|
|
@@ -222,7 +211,7 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
|
|
|
222
211
|
result.updated.push(target);
|
|
223
212
|
} else {
|
|
224
213
|
result.conflicts.push(target);
|
|
225
|
-
if (
|
|
214
|
+
if (isManagedCodexAgentFileEntry(name, previousManagedFiles?.[name])) {
|
|
226
215
|
result.managedFiles[name] = previousManagedFiles[name];
|
|
227
216
|
}
|
|
228
217
|
continue;
|
|
@@ -237,11 +226,11 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
|
|
|
237
226
|
? Object.entries(previousManagedFiles).sort(([left], [right]) => left.localeCompare(right))
|
|
238
227
|
: [];
|
|
239
228
|
for (const [name, previousHash] of previousEntries) {
|
|
240
|
-
if (bundledNames.has(name) || !
|
|
229
|
+
if (bundledNames.has(name) || !isManagedCodexAgentFileEntry(name, previousHash)) continue;
|
|
241
230
|
const target = join(targetDir, name);
|
|
242
231
|
const current = await readCodexAgentTarget(target);
|
|
243
232
|
if (!current.exists) continue;
|
|
244
|
-
if (current.content &&
|
|
233
|
+
if (current.content && hashBytes(current.content) === previousHash) {
|
|
245
234
|
await unlink(target);
|
|
246
235
|
continue;
|
|
247
236
|
}
|
|
@@ -291,17 +280,15 @@ async function getExistingCodexApiConfig(paths = defaultPaths) {
|
|
|
291
280
|
|
|
292
281
|
async function persistCodexConfiguration(paths, { content, auth }) {
|
|
293
282
|
await writeText(paths.codexConfigPath, content);
|
|
294
|
-
await
|
|
283
|
+
await writeJson(paths.codexAuthPath, auth, { sensitive: true });
|
|
295
284
|
}
|
|
296
285
|
|
|
297
286
|
async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}) {
|
|
298
287
|
const {
|
|
299
288
|
assertNotCancelled,
|
|
300
289
|
confirmOrCancel,
|
|
301
|
-
|
|
302
|
-
required
|
|
303
|
-
requiredUnlessExisting,
|
|
304
|
-
resolvePasswordValue
|
|
290
|
+
passwordOrExisting,
|
|
291
|
+
required
|
|
305
292
|
} = promptApi;
|
|
306
293
|
const existing = await getExistingCodexApiConfig(paths);
|
|
307
294
|
const providerId = existing.providerId || "abelworkflow";
|
|
@@ -314,13 +301,10 @@ async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}
|
|
|
314
301
|
assertNotCancelled(baseUrlInput);
|
|
315
302
|
const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
|
|
316
303
|
|
|
317
|
-
const
|
|
318
|
-
"Codex 第三方 API Key",
|
|
319
|
-
existing.apiKey
|
|
320
|
-
|
|
321
|
-
));
|
|
322
|
-
assertNotCancelled(apiKey);
|
|
323
|
-
const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
|
|
304
|
+
const finalApiKey = await passwordOrExisting({
|
|
305
|
+
message: "Codex 第三方 API Key",
|
|
306
|
+
existingValue: existing.apiKey
|
|
307
|
+
});
|
|
324
308
|
|
|
325
309
|
const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
|
|
326
310
|
let managedCodexAgentFiles = { ...(ownership.managedCodexAgentFiles ?? {}) };
|
|
@@ -389,13 +373,16 @@ function buildCodexConfigContent(currentContent, {
|
|
|
389
373
|
if (includeSubagentDefaults && readTopLevelTomlString(content, "approvals_reviewer") === "reviewer") {
|
|
390
374
|
content = updateTopLevelTomlField(content, "approvals_reviewer", "guardian_subagent");
|
|
391
375
|
}
|
|
376
|
+
const providerSectionName = getCodexProviderSectionName(providerId);
|
|
392
377
|
content = updateTopLevelTomlField(content, "model_provider", providerId);
|
|
393
|
-
content =
|
|
394
|
-
content =
|
|
378
|
+
content = removeTopLevelTomlField(content, "preferred_auth_method");
|
|
379
|
+
content = removeTopLevelTomlField(content, "temp_env_key");
|
|
380
|
+
content = removeTomlSectionField(content, providerSectionName, "temp_env_key");
|
|
381
|
+
content = removeTomlSectionField(content, providerSectionName, "env_key");
|
|
382
|
+
content = updateTomlSectionFields(content, providerSectionName, {
|
|
395
383
|
name: providerName,
|
|
396
384
|
base_url: baseUrl,
|
|
397
385
|
wire_api: "responses",
|
|
398
|
-
temp_env_key: CODEX_ENV_KEY,
|
|
399
386
|
requires_openai_auth: true,
|
|
400
387
|
supports_websockets: true
|
|
401
388
|
});
|
|
@@ -408,8 +395,13 @@ function mergeCodexAuthData(auth, envKey, apiKey, managedAuthKeys = []) {
|
|
|
408
395
|
for (const managedAuthKey of managedAuthKeys) {
|
|
409
396
|
delete nextAuth[managedAuthKey];
|
|
410
397
|
}
|
|
411
|
-
if (apiKey)
|
|
412
|
-
|
|
398
|
+
if (apiKey) {
|
|
399
|
+
nextAuth.auth_mode = "apikey";
|
|
400
|
+
nextAuth[envKey] = apiKey;
|
|
401
|
+
} else {
|
|
402
|
+
delete nextAuth[envKey];
|
|
403
|
+
if (nextAuth.auth_mode === "apikey") delete nextAuth.auth_mode;
|
|
404
|
+
}
|
|
413
405
|
return nextAuth;
|
|
414
406
|
}
|
|
415
407
|
|
package/lib/providers/pi.mjs
CHANGED
|
@@ -5,12 +5,16 @@ import {
|
|
|
5
5
|
readJsonFileSafe,
|
|
6
6
|
readJsoncFileSafe,
|
|
7
7
|
updateLockedJson,
|
|
8
|
-
|
|
8
|
+
writeJson,
|
|
9
9
|
writeText
|
|
10
10
|
} from "../config/store.mjs";
|
|
11
11
|
import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
|
|
12
12
|
|
|
13
13
|
const minimumPiVersion = [0, 80, 0];
|
|
14
|
+
const piBootstrapProviderId = "gpt";
|
|
15
|
+
const piBootstrapApi = "openai-completions";
|
|
16
|
+
const piBootstrapBaseUrl = "https://api.openai.com/v1";
|
|
17
|
+
const piBootstrapModelId = "gpt-5.5";
|
|
14
18
|
const piRpcRequestId = "abelworkflow-provider";
|
|
15
19
|
const piRpcArgs = [
|
|
16
20
|
"--mode", "rpc",
|
|
@@ -68,15 +72,28 @@ function assertSupportedPiVersion(value) {
|
|
|
68
72
|
return version;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
function getPiProcessInvocation(args, {
|
|
76
|
+
platform = process.platform,
|
|
77
|
+
comspec = process.env.ComSpec || process.env.COMSPEC || "cmd.exe"
|
|
78
|
+
} = {}) {
|
|
79
|
+
return platform === "win32"
|
|
80
|
+
? { command: comspec, args: ["/d", "/c", "pi", ...args] }
|
|
81
|
+
: { command: "pi", args };
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
function detectPiVersion() {
|
|
72
|
-
const
|
|
85
|
+
const invocation = getPiProcessInvocation(["--version"]);
|
|
86
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
73
87
|
encoding: "utf8",
|
|
74
|
-
shell: process.platform === "win32",
|
|
75
88
|
stdio: ["ignore", "pipe", "pipe"]
|
|
76
89
|
});
|
|
77
90
|
return result.status === 0 ? `${result.stdout || ""} ${result.stderr || ""}`.trim() : undefined;
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
function isPiUnknownModel(model) {
|
|
94
|
+
return model?.provider === "unknown" && model?.id === "unknown" && model?.api === "unknown";
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
function parsePiRpcEffectiveModel(value) {
|
|
81
98
|
for (const line of String(value || "").split(/\r?\n/u)) {
|
|
82
99
|
let payload;
|
|
@@ -85,27 +102,26 @@ function parsePiRpcEffectiveModel(value) {
|
|
|
85
102
|
} catch {
|
|
86
103
|
continue;
|
|
87
104
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
id,
|
|
99
|
-
api: typeof model.api === "string" ? model.api.trim() : "",
|
|
100
|
-
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl.trim() : ""
|
|
105
|
+
if (payload?.type !== "response"
|
|
106
|
+
|| payload.command !== "get_state"
|
|
107
|
+
|| payload.success !== true) continue;
|
|
108
|
+
const model = payload.data?.model;
|
|
109
|
+
if (model === null) return null;
|
|
110
|
+
const effectiveModel = {
|
|
111
|
+
provider: typeof model?.provider === "string" ? model.provider.trim() : "",
|
|
112
|
+
id: typeof model?.id === "string" ? model.id.trim() : "",
|
|
113
|
+
api: typeof model?.api === "string" ? model.api.trim() : "",
|
|
114
|
+
baseUrl: typeof model?.baseUrl === "string" ? model.baseUrl.trim() : ""
|
|
101
115
|
};
|
|
116
|
+
if (isPiUnknownModel(effectiveModel)) return null;
|
|
117
|
+
if (effectiveModel.provider && effectiveModel.id) return effectiveModel;
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
function runPiRpcCommand(command, args, {
|
|
106
122
|
input = "",
|
|
107
123
|
maxBuffer = 1024 * 1024,
|
|
108
|
-
|
|
124
|
+
platform = process.platform,
|
|
109
125
|
start = spawn,
|
|
110
126
|
timeout = 20000
|
|
111
127
|
} = {}) {
|
|
@@ -129,9 +145,11 @@ function runPiRpcCommand(command, args, {
|
|
|
129
145
|
try {
|
|
130
146
|
const env = { ...process.env };
|
|
131
147
|
delete env.NODE_TEST_CONTEXT;
|
|
132
|
-
|
|
148
|
+
const invocation = command === "pi"
|
|
149
|
+
? getPiProcessInvocation(args, { platform })
|
|
150
|
+
: { command, args };
|
|
151
|
+
child = start(invocation.command, invocation.args, {
|
|
133
152
|
env,
|
|
134
|
-
shell,
|
|
135
153
|
stdio: ["pipe", "pipe", "ignore"],
|
|
136
154
|
windowsHide: true
|
|
137
155
|
});
|
|
@@ -148,7 +166,7 @@ function runPiRpcCommand(command, args, {
|
|
|
148
166
|
stdout += chunk;
|
|
149
167
|
if (Buffer.byteLength(stdout, "utf8") > maxBuffer) {
|
|
150
168
|
finish(null);
|
|
151
|
-
} else if (parsePiRpcEffectiveModel(stdout)) {
|
|
169
|
+
} else if (parsePiRpcEffectiveModel(stdout) !== undefined) {
|
|
152
170
|
finish(0);
|
|
153
171
|
}
|
|
154
172
|
});
|
|
@@ -166,7 +184,7 @@ async function detectPiEffectiveModel(run = runPiRpcCommand) {
|
|
|
166
184
|
encoding: "utf8",
|
|
167
185
|
input: `${JSON.stringify({ id: piRpcRequestId, type: "get_state" })}\n`,
|
|
168
186
|
maxBuffer: 1024 * 1024,
|
|
169
|
-
|
|
187
|
+
platform: process.platform,
|
|
170
188
|
timeout: 20000
|
|
171
189
|
});
|
|
172
190
|
} catch {
|
|
@@ -281,6 +299,37 @@ function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}, auth = {},
|
|
|
281
299
|
};
|
|
282
300
|
}
|
|
283
301
|
|
|
302
|
+
function resolvePiApiTarget(modelsConfig = {}, settings = {}, auth = {}, effectiveModel) {
|
|
303
|
+
const hasNoEffectiveModel = effectiveModel === null || isPiUnknownModel(effectiveModel);
|
|
304
|
+
const configuration = resolveExistingPiApiConfig(
|
|
305
|
+
modelsConfig,
|
|
306
|
+
settings,
|
|
307
|
+
auth,
|
|
308
|
+
hasNoEffectiveModel ? undefined : effectiveModel
|
|
309
|
+
);
|
|
310
|
+
if (configuration.providerId || !hasNoEffectiveModel) {
|
|
311
|
+
return { bootstrap: false, configuration };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const provider = modelsConfig.providers?.[piBootstrapProviderId];
|
|
315
|
+
const models = Array.isArray(provider?.models) ? provider.models.filter((model) => model?.id) : [];
|
|
316
|
+
const credential = auth[piBootstrapProviderId];
|
|
317
|
+
const apiKey = credential?.type === "api_key" && typeof credential.key === "string"
|
|
318
|
+
? credential.key
|
|
319
|
+
: typeof provider?.apiKey === "string" ? provider.apiKey : "";
|
|
320
|
+
return {
|
|
321
|
+
bootstrap: true,
|
|
322
|
+
configuration: {
|
|
323
|
+
providerId: piBootstrapProviderId,
|
|
324
|
+
baseUrl: typeof provider?.baseUrl === "string" ? provider.baseUrl : piBootstrapBaseUrl,
|
|
325
|
+
api: typeof provider?.api === "string" ? provider.api : piBootstrapApi,
|
|
326
|
+
apiKey,
|
|
327
|
+
modelIds: models.map((model) => model.id),
|
|
328
|
+
defaultModel: models[0]?.id || piBootstrapModelId
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
284
333
|
function assertConfigurablePiProvider(modelsConfig = {}, configuration = {}) {
|
|
285
334
|
const { providerId, defaultModel, api } = configuration;
|
|
286
335
|
const provider = modelsConfig.providers?.[providerId];
|
|
@@ -412,26 +461,27 @@ async function persistPiConfiguration(paths, configuration, operations = {}) {
|
|
|
412
461
|
`${JSON.stringify(value, null, 2)}\n`,
|
|
413
462
|
{ sensitive: true }
|
|
414
463
|
));
|
|
415
|
-
const writeSettings = operations.writeSettings ?? ((path, value) =>
|
|
464
|
+
const writeSettings = operations.writeSettings ?? ((path, value) => writeJson(path, value));
|
|
416
465
|
await updateAuth(paths.piAuthPath, configuration.providerId, configuration.apiKey);
|
|
417
466
|
await writeModels(paths.piModelsPath, configuration.models);
|
|
418
467
|
await writeSettings(paths.piSettingsPath, configuration.settings);
|
|
419
468
|
}
|
|
420
469
|
|
|
421
470
|
async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = async () => {}, promptApi, runtime = {}) {
|
|
471
|
+
const log = runtime.log ?? p.log;
|
|
472
|
+
const spinner = runtime.spinner ?? p.spinner;
|
|
473
|
+
const text = runtime.text ?? p.text;
|
|
422
474
|
const piVersion = await (runtime.getPiVersion ?? detectPiVersion)();
|
|
423
475
|
try {
|
|
424
476
|
assertSupportedPiVersion(piVersion);
|
|
425
477
|
} catch (error) {
|
|
426
|
-
|
|
478
|
+
log.warn(error.message || String(error));
|
|
427
479
|
return;
|
|
428
480
|
}
|
|
429
481
|
const {
|
|
430
482
|
assertNotCancelled,
|
|
431
|
-
|
|
483
|
+
passwordOrExisting,
|
|
432
484
|
required,
|
|
433
|
-
requiredUnlessExisting,
|
|
434
|
-
resolvePasswordValue,
|
|
435
485
|
selectOrCancel
|
|
436
486
|
} = promptApi;
|
|
437
487
|
const {
|
|
@@ -439,7 +489,7 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
|
|
|
439
489
|
models: modelsConfig,
|
|
440
490
|
settings
|
|
441
491
|
} = await readExistingPiConfiguration(paths);
|
|
442
|
-
const detectionSpinner =
|
|
492
|
+
const detectionSpinner = spinner();
|
|
443
493
|
detectionSpinner.start("正在识别 Pi 当前有效模型");
|
|
444
494
|
let effectiveModel;
|
|
445
495
|
try {
|
|
@@ -449,16 +499,21 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
|
|
|
449
499
|
? `已识别 ${effectiveModel.provider}/${effectiveModel.id}`
|
|
450
500
|
: "未识别到 Pi 当前有效模型");
|
|
451
501
|
}
|
|
452
|
-
const
|
|
502
|
+
const target = resolvePiApiTarget(modelsConfig, settings, auth, effectiveModel);
|
|
503
|
+
const existing = target.configuration;
|
|
453
504
|
const providerId = requirePiProviderId(existing.providerId);
|
|
454
|
-
|
|
505
|
+
if (target.bootstrap) {
|
|
506
|
+
log.info(`Pi 尚未配置可用模型,将创建自定义 Provider ${providerId}。`);
|
|
507
|
+
} else {
|
|
508
|
+
assertConfigurablePiProvider(modelsConfig, existing);
|
|
509
|
+
}
|
|
455
510
|
if (effectiveModel
|
|
456
511
|
&& (settings.defaultProvider !== existing.providerId || settings.defaultModel !== existing.defaultModel)) {
|
|
457
|
-
|
|
512
|
+
log.warn(`Pi 保存的默认模型 ${settings.defaultProvider || "未知"}/${settings.defaultModel || "未知"} 与当前有效模型 ${existing.providerId}/${existing.defaultModel} 不同;将配置当前有效模型。`);
|
|
458
513
|
}
|
|
459
514
|
const providerLabel = `Pi ${providerId}`;
|
|
460
515
|
|
|
461
|
-
const baseUrlInput = await
|
|
516
|
+
const baseUrlInput = await text({
|
|
462
517
|
message: `${providerLabel} Base URL`,
|
|
463
518
|
initialValue: existing.baseUrl,
|
|
464
519
|
validate: required()
|
|
@@ -477,15 +532,12 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
|
|
|
477
532
|
...(initialApi ? { initialValue: initialApi } : {})
|
|
478
533
|
});
|
|
479
534
|
|
|
480
|
-
const
|
|
481
|
-
`${providerLabel} API Key`,
|
|
482
|
-
existing.apiKey
|
|
483
|
-
|
|
484
|
-
));
|
|
485
|
-
assertNotCancelled(apiKey);
|
|
486
|
-
const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
|
|
535
|
+
const finalApiKey = await passwordOrExisting({
|
|
536
|
+
message: `${providerLabel} API Key`,
|
|
537
|
+
existingValue: existing.apiKey
|
|
538
|
+
});
|
|
487
539
|
|
|
488
|
-
const modelIdsText = await
|
|
540
|
+
const modelIdsText = await text({
|
|
489
541
|
message: `${providerLabel} 模型 ID(多个用逗号分隔)`,
|
|
490
542
|
initialValue: (existing.modelIds.length
|
|
491
543
|
? existing.modelIds
|
|
@@ -495,7 +547,7 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
|
|
|
495
547
|
assertNotCancelled(modelIdsText);
|
|
496
548
|
const modelIds = parsePiModelIds(modelIdsText);
|
|
497
549
|
|
|
498
|
-
const defaultModel = await
|
|
550
|
+
const defaultModel = await text({
|
|
499
551
|
message: "Pi 默认模型",
|
|
500
552
|
initialValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
|
|
501
553
|
validate: (value) => modelIds.includes(String(value || "").trim()) ? undefined : "默认模型必须在模型 ID 列表中"
|
|
@@ -514,10 +566,10 @@ async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = as
|
|
|
514
566
|
await ensurePiResourcesLinked(paths);
|
|
515
567
|
await persistPiConfiguration(paths, configuration);
|
|
516
568
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
569
|
+
log.step(`已更新 ${pathToLabel(paths.piModelsPath, paths.homeDir)} (${providerId}, ${baseUrl})`);
|
|
570
|
+
log.step(`已更新 ${pathToLabel(paths.piSettingsPath, paths.homeDir)} (默认模型: ${finalDefaultModel})`);
|
|
571
|
+
log.step(`已更新 ${pathToLabel(paths.piAuthPath, paths.homeDir)} (${maskSecret(finalApiKey)})`);
|
|
572
|
+
log.step(`已链接 Pi 扩展到 ${pathToLabel(join(paths.piAgentDir, "extensions"), paths.homeDir)}`);
|
|
521
573
|
}
|
|
522
574
|
|
|
523
575
|
export {
|
|
@@ -530,6 +582,7 @@ export {
|
|
|
530
582
|
configurePiApi,
|
|
531
583
|
detectPiEffectiveModel,
|
|
532
584
|
getPiApiPromptOptions,
|
|
585
|
+
getPiProcessInvocation,
|
|
533
586
|
inferPiApiFromBaseUrl,
|
|
534
587
|
normalizeOpenAiBaseUrl,
|
|
535
588
|
parsePiRpcEffectiveModel,
|
|
@@ -537,6 +590,7 @@ export {
|
|
|
537
590
|
persistPiConfiguration,
|
|
538
591
|
readExistingPiConfiguration,
|
|
539
592
|
resolveExistingPiApiConfig,
|
|
593
|
+
resolvePiApiTarget,
|
|
540
594
|
runPiRpcCommand,
|
|
541
595
|
updatePiAuthFile
|
|
542
596
|
};
|
package/lib/providers/skills.mjs
CHANGED
|
@@ -21,10 +21,8 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
|
|
|
21
21
|
const {
|
|
22
22
|
assertNotCancelled,
|
|
23
23
|
confirmOrCancel,
|
|
24
|
-
|
|
25
|
-
required
|
|
26
|
-
requiredUnlessExisting,
|
|
27
|
-
resolvePasswordValue
|
|
24
|
+
passwordOrExisting,
|
|
25
|
+
required
|
|
28
26
|
} = promptApi;
|
|
29
27
|
await ensureSkillPresent(paths);
|
|
30
28
|
const envPath = join(paths.agentsDir, "skills", "grok-search", ".env");
|
|
@@ -36,13 +34,11 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
|
|
|
36
34
|
});
|
|
37
35
|
assertNotCancelled(baseUrl);
|
|
38
36
|
|
|
39
|
-
const
|
|
40
|
-
"Grok API Key",
|
|
41
|
-
existing.GROK_API_KEY,
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
assertNotCancelled(apiKey);
|
|
45
|
-
const finalApiKey = resolvePasswordValue(apiKey, existing.GROK_API_KEY);
|
|
37
|
+
const finalApiKey = await passwordOrExisting({
|
|
38
|
+
message: "Grok API Key",
|
|
39
|
+
existingValue: existing.GROK_API_KEY,
|
|
40
|
+
requiredMessage: "Grok API Key 不能为空"
|
|
41
|
+
});
|
|
46
42
|
|
|
47
43
|
const model = await p.text({
|
|
48
44
|
message: "Grok 默认模型",
|
|
@@ -65,15 +61,13 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
|
|
|
65
61
|
: null;
|
|
66
62
|
if (useTavily) assertNotCancelled(tavilyUrl);
|
|
67
63
|
|
|
68
|
-
const
|
|
69
|
-
? await
|
|
70
|
-
"Tavily API Key",
|
|
71
|
-
existing.TAVILY_API_KEY,
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
:
|
|
75
|
-
if (useTavily) assertNotCancelled(tavilyKey);
|
|
76
|
-
const finalTavilyKey = useTavily ? resolvePasswordValue(tavilyKey, existing.TAVILY_API_KEY) : null;
|
|
64
|
+
const finalTavilyKey = useTavily
|
|
65
|
+
? await passwordOrExisting({
|
|
66
|
+
message: "Tavily API Key",
|
|
67
|
+
existingValue: existing.TAVILY_API_KEY,
|
|
68
|
+
requiredMessage: "Tavily API Key 不能为空"
|
|
69
|
+
})
|
|
70
|
+
: null;
|
|
77
71
|
|
|
78
72
|
await updateSkillEnvFile(envPath, {
|
|
79
73
|
GROK_API_URL: baseUrl,
|
|
@@ -88,16 +82,15 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
|
|
|
88
82
|
}
|
|
89
83
|
|
|
90
84
|
async function configureContext7Env(paths, ensureSkillPresent = async () => {}, promptApi) {
|
|
91
|
-
const {
|
|
85
|
+
const { passwordOrExisting } = promptApi;
|
|
92
86
|
await ensureSkillPresent(paths);
|
|
93
87
|
const envPath = join(paths.agentsDir, "skills", "context7-auto-research", ".env");
|
|
94
88
|
const existing = await readSkillEnvFile(envPath);
|
|
95
|
-
const
|
|
96
|
-
"Context7 API Key(可选)",
|
|
97
|
-
existing.CONTEXT7_API_KEY
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const finalApiKey = resolvePasswordValue(apiKey, existing.CONTEXT7_API_KEY);
|
|
89
|
+
const finalApiKey = await passwordOrExisting({
|
|
90
|
+
message: "Context7 API Key(可选)",
|
|
91
|
+
existingValue: existing.CONTEXT7_API_KEY,
|
|
92
|
+
requiredMessage: null
|
|
93
|
+
});
|
|
101
94
|
|
|
102
95
|
await updateSkillEnvFile(envPath, {
|
|
103
96
|
CONTEXT7_API_KEY: finalApiKey
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
personality = "pragmatic"
|
|
2
2
|
model_provider = "abelworkflow"
|
|
3
3
|
disable_response_storage = true
|
|
4
|
-
preferred_auth_method = "apikey"
|
|
5
4
|
approvals_reviewer = "guardian_subagent"
|
|
6
5
|
approval_policy = "on-request"
|
|
7
|
-
sandbox_mode = "
|
|
6
|
+
sandbox_mode = "danger-full-access"
|
|
8
7
|
model = "gpt-5.6-sol"
|
|
9
8
|
model_reasoning_effort = "high"
|
|
10
9
|
network_access = true
|
package/lib/utils.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
function hashBytes(content) {
|
|
4
|
+
return createHash("sha256").update(content).digest("hex");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isManagedCodexAgentFileEntry(name, hash) {
|
|
8
|
+
return name.endsWith(".toml")
|
|
9
|
+
&& !name.includes("/")
|
|
10
|
+
&& !name.includes("\\")
|
|
11
|
+
&& typeof hash === "string"
|
|
12
|
+
&& /^[a-f0-9]{64}$/u.test(hash);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { hashBytes, isManagedCodexAgentFileEntry };
|