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 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,并将其 API Key 保存在 `~/.pi/agent/auth.json`;`models.json` 只保留模型定义。旧 models-only key 会按先写 auth、后删除旧 key 的顺序迁移。
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 configureCodexForPaths(paths) {
129
+ async function configureProviderWithMetadata(paths, configure) {
130
130
  const metadata = await readInstallMetadata(paths);
131
131
  const packageVersion = await packageVersionFor(paths, metadata);
132
- const result = await configureCodexApi(paths, promptApi, {
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
- const metadata = await readInstallMetadata(paths);
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, ensurePiResourcesLinked, promptApi);
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) => ensureSkillPresent(paths, "grok-search"), promptApi);
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) => ensureSkillPresent(paths, "context7-auto-research"), promptApi);
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
- "pi-cli": {
209
- tool: "pi",
209
+ pi: {
210
210
  title: "Pi",
211
- actions: {
212
- "pi-install": async () => installCliTool("pi", promptApi),
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
- "codex-cli": {
217
- tool: "codex",
214
+ codex: {
218
215
  title: "Codex",
219
- actions: {
220
- "codex-install": async () => installCliTool("codex", promptApi),
221
- "codex-api": async () => configureCodexForPaths(options.paths)
222
- }
216
+ install: () => installCliTool("codex", promptApi),
217
+ configure: () => configureCodexForPaths(options.paths)
223
218
  },
224
- "claude-cli": {
225
- tool: "claude",
219
+ claude: {
226
220
  title: "Claude Code",
227
- actions: {
228
- "claude-install": async () => installCliTool("claude", promptApi),
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 ({ tool, title, actions }) => {
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(cliToolMenus["pi-cli"]),
263
- "codex-cli": async () => runCliToolMenu(cliToolMenus["codex-cli"]),
264
- "claude-cli": async () => runCliToolMenu(cliToolMenus["claude-cli"])
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 };
@@ -11,22 +11,10 @@ const interactiveMenuDescriptors = [
11
11
  { value: "exit", label: "退出", group: "exit" }
12
12
  ];
13
13
  const interactiveMenuDefaultValue = "full-init";
14
- const cliToolMenuDescriptorMap = {
15
- pi: [
16
- { value: "pi-api", label: "配置 Pi API" },
17
- { value: "pi-install", label: "安装/更新 Pi" },
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 descriptors = cliToolMenuDescriptorMap[tool];
40
- if (!descriptors) throw new Error(`Unknown CLI tool: ${tool}`);
41
- return descriptors.map((descriptor) => ({ ...descriptor }));
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,
@@ -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, renderDotenv, updateDotenvContent };
94
+ export { parseDotenv, quoteEnvValue, updateDotenvContent };
@@ -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
  };
@@ -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
  };
@@ -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 { join } from "node:path";
2
- import { pathExists, readJsonFileSafe } from "../config/store.mjs";
3
- import { createPaths, defaultPaths } from "../paths.mjs";
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
- const packagePath = join(paths.packageRoot, "package.json");
62
- const packageData = await readJsonFileSafe(packagePath);
63
- if (typeof packageData.version !== "string" || !packageData.version) {
64
- throw new Error(`Missing package version in ${packagePath}`);
65
- }
66
- return packageData.version;
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
- const linkedTargets = mergeLinkedTargets(previousLinkedTargets, linkResults);
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: resolveInstallPaths(options) });
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
- const metadata = buildInstallMetadata({
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
- const metadata = buildInstallMetadata({
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
 
@@ -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 { ensureManagedContainerDirectory, hashBytes } from "./assets.mjs";
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, kind) {
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, kind);
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
- const relativePath = pathOps.relative(managedSourceRoot, targetPath);
262
- return relativePath !== ""
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 = {}) {
@@ -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.endsWith(".toml")
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
- const metadata = {
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,
@@ -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 writeJsonFileWithBackup(paths.claudeSettingsPath, settings, { sensitive: true });
115
- await writeJsonFileWithBackup(paths.claudeMetaConfigPath, metaConfig, { sensitive: true });
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 key = await p.password(passwordPromptOptions(
137
- "Claude Code API Key",
138
- existing.key,
139
- requiredUnlessExisting(existing.key, "API Key 不能为空")
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 模型",
@@ -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
- writeJsonFileWithBackup,
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
- sha256(currentDeveloperInstructions)
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 = sha256(sourceContent);
197
+ const sourceHash = hashBytes(sourceContent);
209
198
  const current = await readCodexAgentTarget(target);
210
- const currentHash = current.content ? sha256(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 (isCodexAgentOwnership(name, previousManagedFiles?.[name])) {
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) || !isCodexAgentOwnership(name, previousHash)) continue;
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 && sha256(current.content) === previousHash) {
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 writeJsonFileWithBackup(paths.codexAuthPath, auth, { sensitive: true });
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
- passwordPromptOptions,
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 apiKey = await p.password(passwordPromptOptions(
318
- "Codex 第三方 API Key",
319
- existing.apiKey,
320
- requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
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 = updateTopLevelTomlField(content, "preferred_auth_method", "apikey");
394
- content = updateTomlSectionFields(content, getCodexProviderSectionName(providerId), {
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) nextAuth[envKey] = apiKey;
412
- else delete nextAuth[envKey];
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
 
@@ -5,12 +5,16 @@ import {
5
5
  readJsonFileSafe,
6
6
  readJsoncFileSafe,
7
7
  updateLockedJson,
8
- writeJsonFileWithBackup,
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 result = spawnSync("pi", ["--version"], {
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
- const model = payload?.type === "response"
89
- && payload.command === "get_state"
90
- && payload.success === true
91
- ? payload.data?.model
92
- : undefined;
93
- const provider = typeof model?.provider === "string" ? model.provider.trim() : "";
94
- const id = typeof model?.id === "string" ? model.id.trim() : "";
95
- if (!provider || !id) continue;
96
- return {
97
- provider,
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
- shell = process.platform === "win32",
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
- child = start(command, args, {
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
- shell: process.platform === "win32",
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) => writeJsonFileWithBackup(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
- (runtime.log ?? p.log).warn(error.message || String(error));
478
+ log.warn(error.message || String(error));
427
479
  return;
428
480
  }
429
481
  const {
430
482
  assertNotCancelled,
431
- passwordPromptOptions,
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 = p.spinner();
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 existing = resolveExistingPiApiConfig(modelsConfig, settings, auth, effectiveModel);
502
+ const target = resolvePiApiTarget(modelsConfig, settings, auth, effectiveModel);
503
+ const existing = target.configuration;
453
504
  const providerId = requirePiProviderId(existing.providerId);
454
- assertConfigurablePiProvider(modelsConfig, existing);
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
- p.log.warn(`Pi 保存的默认模型 ${settings.defaultProvider || "未知"}/${settings.defaultModel || "未知"} 与当前有效模型 ${existing.providerId}/${existing.defaultModel} 不同;将配置当前有效模型。`);
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 p.text({
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 apiKey = await p.password(passwordPromptOptions(
481
- `${providerLabel} API Key`,
482
- existing.apiKey,
483
- requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
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 p.text({
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 p.text({
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
- p.log.step(`已更新 ${pathToLabel(paths.piModelsPath, paths.homeDir)} (${providerId}, ${baseUrl})`);
518
- p.log.step(`已更新 ${pathToLabel(paths.piSettingsPath, paths.homeDir)} (默认模型: ${finalDefaultModel})`);
519
- p.log.step(`已更新 ${pathToLabel(paths.piAuthPath, paths.homeDir)} (${maskSecret(finalApiKey)})`);
520
- p.log.step(`已链接 Pi 扩展到 ${pathToLabel(join(paths.piAgentDir, "extensions"), paths.homeDir)}`);
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
  };
@@ -21,10 +21,8 @@ async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}
21
21
  const {
22
22
  assertNotCancelled,
23
23
  confirmOrCancel,
24
- passwordPromptOptions,
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 apiKey = await p.password(passwordPromptOptions(
40
- "Grok API Key",
41
- existing.GROK_API_KEY,
42
- requiredUnlessExisting(existing.GROK_API_KEY, "Grok API Key 不能为空")
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 tavilyKey = useTavily
69
- ? await p.password(passwordPromptOptions(
70
- "Tavily API Key",
71
- existing.TAVILY_API_KEY,
72
- requiredUnlessExisting(existing.TAVILY_API_KEY, "Tavily API Key 不能为空")
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 { assertNotCancelled, passwordPromptOptions, resolvePasswordValue } = promptApi;
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 apiKey = await p.password(passwordPromptOptions(
96
- "Context7 API Key(可选)",
97
- existing.CONTEXT7_API_KEY
98
- ));
99
- assertNotCancelled(apiKey);
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 = "workspace-write"
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelworkflow",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Install AbelWorkflow into ~/.agents and create Claude/Codex symlinks.",
5
5
  "type": "module",
6
6
  "scripts": {