abelworkflow 1.1.3 → 1.2.0

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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -29
  3. package/extensions/gpt-responses-compat.ts +213 -0
  4. package/extensions/k2think-compat.ts +40 -0
  5. package/lib/cli/args.mjs +5 -19
  6. package/lib/cli/main.mjs +105 -72
  7. package/lib/cli/prompts.mjs +9 -2
  8. package/lib/config/dotenv.mjs +1 -1
  9. package/lib/config/jsonc.mjs +1 -1
  10. package/lib/config/store.mjs +197 -45
  11. package/lib/config/toml.mjs +147 -453
  12. package/lib/installer/assets.mjs +67 -45
  13. package/lib/installer/install.mjs +202 -45
  14. package/lib/installer/links.mjs +124 -101
  15. package/lib/installer/lock.mjs +82 -0
  16. package/lib/installer/state.mjs +14 -21
  17. package/lib/paths.mjs +9 -9
  18. package/lib/providers/claude.mjs +58 -114
  19. package/lib/providers/codex.mjs +101 -198
  20. package/lib/providers/pi.mjs +331 -497
  21. package/lib/providers/skills.mjs +88 -68
  22. package/lib/providers/url.mjs +35 -0
  23. package/lib/templates/workflow/commands/abel-design.md +23 -161
  24. package/lib/templates/workflow/commands/abel-diagnose.md +30 -56
  25. package/lib/templates/workflow/commands/abel-implement.md +30 -150
  26. package/lib/templates/workflow/commands/abel-init.md +1 -1
  27. package/lib/tools/cli-installer.mjs +29 -190
  28. package/package.json +7 -9
  29. package/skills/context7-auto-research/context7-api.cjs +75 -17
  30. package/skills/dev-browser/SKILL.md +15 -1
  31. package/skills/dev-browser/dist/scripts/start.d.ts +0 -1
  32. package/skills/dev-browser/dist/scripts/start.js +8 -36
  33. package/skills/dev-browser/dist/src/client.d.ts +3 -4
  34. package/skills/dev-browser/dist/src/client.js +117 -117
  35. package/skills/dev-browser/dist/src/entrypoint.d.ts +1 -1
  36. package/skills/dev-browser/dist/src/entrypoint.js +38 -31
  37. package/skills/dev-browser/dist/src/index.d.ts +0 -1
  38. package/skills/dev-browser/dist/src/index.js +0 -1
  39. package/skills/dev-browser/dist/src/page-api.d.ts +0 -1
  40. package/skills/dev-browser/dist/src/page-api.js +0 -1
  41. package/skills/dev-browser/dist/src/relay.d.ts +0 -1
  42. package/skills/dev-browser/dist/src/relay.js +53 -7
  43. package/skills/dev-browser/dist/src/runtime.d.ts +3 -14
  44. package/skills/dev-browser/dist/src/runtime.js +5 -15
  45. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +0 -1
  46. package/skills/dev-browser/dist/src/snapshot/browser-script.js +0 -1
  47. package/skills/dev-browser/dist/src/snapshot/index.d.ts +0 -1
  48. package/skills/dev-browser/dist/src/snapshot/index.js +0 -1
  49. package/skills/dev-browser/dist/src/snapshot/inject.d.ts +0 -1
  50. package/skills/dev-browser/dist/src/snapshot/inject.js +0 -1
  51. package/skills/dev-browser/dist/src/standalone.d.ts +0 -1
  52. package/skills/dev-browser/dist/src/standalone.js +2 -3
  53. package/skills/dev-browser/dist/src/startup.d.ts +3 -12
  54. package/skills/dev-browser/dist/src/startup.js +6 -10
  55. package/skills/dev-browser/dist/src/target-registry.d.ts +0 -1
  56. package/skills/dev-browser/dist/src/target-registry.js +0 -1
  57. package/skills/dev-browser/dist/src/types.d.ts +0 -1
  58. package/skills/dev-browser/dist/src/types.js +0 -1
  59. package/skills/dev-browser/package-lock.json +1 -1
  60. package/skills/dev-browser/package.json +2 -2
  61. package/skills/grok-search/.env.example +1 -1
  62. package/skills/grok-search/SKILL.md +13 -16
  63. package/skills/grok-search/scripts/_dotenv.py +1 -1
  64. package/skills/grok-search/scripts/groksearch_cli.py +271 -195
  65. package/skills/time/SKILL.md +5 -2
  66. package/skills/time/scripts/time_cli.py +61 -22
  67. package/extensions/pi-gpt-responses-compat/index.ts +0 -25
  68. package/lib/templates/codex/config-base.toml +0 -103
  69. package/skills/dev-browser/dist/scripts/start.d.ts.map +0 -1
  70. package/skills/dev-browser/dist/scripts/start.js.map +0 -1
  71. package/skills/dev-browser/dist/src/client.d.ts.map +0 -1
  72. package/skills/dev-browser/dist/src/client.js.map +0 -1
  73. package/skills/dev-browser/dist/src/entrypoint.d.ts.map +0 -1
  74. package/skills/dev-browser/dist/src/entrypoint.js.map +0 -1
  75. package/skills/dev-browser/dist/src/index.d.ts.map +0 -1
  76. package/skills/dev-browser/dist/src/index.js.map +0 -1
  77. package/skills/dev-browser/dist/src/page-api.d.ts.map +0 -1
  78. package/skills/dev-browser/dist/src/page-api.js.map +0 -1
  79. package/skills/dev-browser/dist/src/relay.d.ts.map +0 -1
  80. package/skills/dev-browser/dist/src/relay.js.map +0 -1
  81. package/skills/dev-browser/dist/src/runtime.d.ts.map +0 -1
  82. package/skills/dev-browser/dist/src/runtime.js.map +0 -1
  83. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +0 -1
  84. package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +0 -1
  85. package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +0 -1
  86. package/skills/dev-browser/dist/src/snapshot/index.js.map +0 -1
  87. package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +0 -1
  88. package/skills/dev-browser/dist/src/snapshot/inject.js.map +0 -1
  89. package/skills/dev-browser/dist/src/standalone.d.ts.map +0 -1
  90. package/skills/dev-browser/dist/src/standalone.js.map +0 -1
  91. package/skills/dev-browser/dist/src/startup.d.ts.map +0 -1
  92. package/skills/dev-browser/dist/src/startup.js.map +0 -1
  93. package/skills/dev-browser/dist/src/target-registry.d.ts.map +0 -1
  94. package/skills/dev-browser/dist/src/target-registry.js.map +0 -1
  95. package/skills/dev-browser/dist/src/types.d.ts.map +0 -1
  96. package/skills/dev-browser/dist/src/types.js.map +0 -1
  97. package/skills/grok-search/gitignore.template +0 -4
  98. package/skills/grok-search/scripts/groksearch_entry.py +0 -131
@@ -1,161 +1,105 @@
1
1
  import * as p from "@clack/prompts";
2
- import { readJsonFileSafe, writeJson } from "../config/store.mjs";
3
- import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
4
-
5
- const claudeModelEnvKeys = [
6
- "ANTHROPIC_MODEL",
7
- "ANTHROPIC_DEFAULT_OPUS_MODEL",
8
- "ANTHROPIC_DEFAULT_SONNET_MODEL",
9
- "ANTHROPIC_DEFAULT_HAIKU_MODEL",
10
- "CLAUDE_CODE_SUBAGENT_MODEL"
11
- ];
12
- const defaultClaudeSettings = {
13
- $schema: "https://json.schemastore.org/claude-code-settings.json",
14
- env: {
15
- DISABLE_TELEMETRY: "1",
16
- DISABLE_ERROR_REPORTING: "1",
17
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
18
- },
19
- includeCoAuthoredBy: false,
20
- permissions: {
21
- allow: [],
22
- deny: [],
23
- defaultMode: "bypassPermissions"
24
- },
25
- hooks: {},
26
- alwaysThinkingEnabled: true,
27
- language: "Chinese"
28
- };
29
- function buildDefaultClaudeSettings() {
30
- return {
31
- ...defaultClaudeSettings,
32
- env: { ...defaultClaudeSettings.env },
33
- permissions: {
34
- ...defaultClaudeSettings.permissions,
35
- allow: [...defaultClaudeSettings.permissions.allow],
36
- deny: [...defaultClaudeSettings.permissions.deny]
37
- },
38
- hooks: { ...defaultClaudeSettings.hooks }
39
- };
40
- }
41
- function mergeClaudeSettingsWithDefaults(settings) {
42
- const defaults = buildDefaultClaudeSettings();
43
- const env = settings?.env && typeof settings.env === "object" ? settings.env : {};
44
- const permissions = settings?.permissions && typeof settings.permissions === "object" ? settings.permissions : {};
45
- return {
46
- ...defaults,
47
- ...settings,
48
- env: {
49
- ...defaults.env,
50
- ...env
51
- },
52
- permissions: {
53
- ...defaults.permissions,
54
- ...permissions,
55
- allow: Array.isArray(permissions.allow) ? permissions.allow : defaults.permissions.allow,
56
- deny: Array.isArray(permissions.deny) ? permissions.deny : defaults.permissions.deny
57
- },
58
- hooks: settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : defaults.hooks
59
- };
60
- }
2
+ import {
3
+ assertPlainObject,
4
+ readJsonFileSafe,
5
+ withFileTransaction
6
+ } from "../config/store.mjs";
7
+ import { maskSecret, pathToLabel } from "../paths.mjs";
8
+ import { normalizeHttpBaseUrl } from "./url.mjs";
61
9
 
62
10
  function getExistingClaudeApiConfig(settings) {
63
- const env = mergeClaudeSettingsWithDefaults(settings).env;
11
+ assertPlainObject(settings, "Claude settings");
12
+ const env = settings.env === undefined ? {} : assertPlainObject(settings.env, "Claude settings env");
64
13
  return {
65
14
  baseUrl: env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
66
15
  key: env.ANTHROPIC_API_KEY || "",
67
- model: claudeModelEnvKeys.map((field) => env[field]).find(Boolean) || ""
16
+ model: typeof env.ANTHROPIC_MODEL === "string" ? env.ANTHROPIC_MODEL : ""
68
17
  };
69
18
  }
70
19
 
71
- function ensureApprovedClaudeApiKey(config, apiKey) {
72
- if (!apiKey) {
73
- return config;
74
- }
75
-
76
- const truncated = apiKey.slice(0, 20);
77
- if (!config.customApiKeyResponses || typeof config.customApiKeyResponses !== "object") {
78
- config.customApiKeyResponses = { approved: [], rejected: [] };
79
- }
80
- if (!Array.isArray(config.customApiKeyResponses.approved)) {
81
- config.customApiKeyResponses.approved = [];
82
- }
83
- if (!Array.isArray(config.customApiKeyResponses.rejected)) {
84
- config.customApiKeyResponses.rejected = [];
85
- }
86
-
87
- config.customApiKeyResponses.rejected = config.customApiKeyResponses.rejected.filter((item) => item !== truncated);
88
- if (!config.customApiKeyResponses.approved.includes(truncated)) {
89
- config.customApiKeyResponses.approved.push(truncated);
90
- }
91
-
92
- return config;
93
- }
94
-
95
20
  function buildClaudeApiSettings(settings, {
96
21
  baseUrl,
97
22
  key,
98
23
  model
99
24
  }) {
100
- const nextSettings = mergeClaudeSettingsWithDefaults(settings);
101
- nextSettings.env.ANTHROPIC_BASE_URL = baseUrl;
102
- nextSettings.env.ANTHROPIC_API_KEY = key;
25
+ assertPlainObject(settings, "Claude settings");
26
+ const env = settings.env === undefined ? {} : assertPlainObject(settings.env, "Claude settings env");
27
+ const nextSettings = {
28
+ ...settings,
29
+ env: {
30
+ ...env,
31
+ ANTHROPIC_BASE_URL: normalizeHttpBaseUrl(baseUrl),
32
+ ANTHROPIC_API_KEY: key,
33
+ ANTHROPIC_MODEL: String(model).trim()
34
+ }
35
+ };
103
36
  delete nextSettings.env.ANTHROPIC_AUTH_TOKEN;
104
- for (const field of claudeModelEnvKeys) {
105
- nextSettings.env[field] = model;
106
- }
107
37
  return nextSettings;
108
38
  }
109
39
 
110
- async function persistClaudeConfiguration(paths, { settings, metaConfig }) {
111
- await writeJson(paths.claudeSettingsPath, settings, { sensitive: true });
112
- await writeJson(paths.claudeMetaConfigPath, metaConfig, { sensitive: true });
40
+ async function persistClaudeConfiguration(paths, intent) {
41
+ return withFileTransaction([{
42
+ path: paths.claudeSettingsPath,
43
+ safeRoot: paths.homeDir,
44
+ sensitive: true
45
+ }], async (transaction) => {
46
+ const settings = transaction.readJson(paths.claudeSettingsPath, {});
47
+ assertPlainObject(settings, "Claude settings");
48
+ await transaction.writeJson(
49
+ paths.claudeSettingsPath,
50
+ buildClaudeApiSettings(settings, intent)
51
+ );
52
+ });
113
53
  }
114
54
 
115
- async function configureClaudeApi(paths = defaultPaths, promptApi) {
116
- const { assertNotCancelled, passwordOrExisting, required } = promptApi;
117
- const settings = await readJsonFileSafe(paths.claudeSettingsPath, {}, { sensitive: true });
55
+ async function configureClaudeApi(paths, promptApi, runtime = {}) {
56
+ const text = runtime.text ?? p.text;
57
+ const log = runtime.log ?? p.log;
58
+ const { assertNotCancelled, passwordOrExisting } = promptApi;
59
+ const settings = await readJsonFileSafe(paths.claudeSettingsPath, {}, {
60
+ safeRoot: paths.homeDir,
61
+ sensitive: true
62
+ });
63
+ assertPlainObject(settings, "Claude settings");
118
64
  const existing = getExistingClaudeApiConfig(settings);
119
65
 
120
- const baseUrl = await p.text({
66
+ const baseUrlInput = await text({
121
67
  message: "Claude Code Base URL",
122
68
  initialValue: existing.baseUrl,
123
- validate: required()
69
+ validate: (value) => {
70
+ try {
71
+ normalizeHttpBaseUrl(value);
72
+ } catch (error) {
73
+ return error.message;
74
+ }
75
+ }
124
76
  });
125
- assertNotCancelled(baseUrl);
77
+ assertNotCancelled(baseUrlInput);
78
+ const baseUrl = normalizeHttpBaseUrl(baseUrlInput);
126
79
 
127
80
  const finalKey = await passwordOrExisting({
128
81
  message: "Claude Code API Key",
129
82
  existingValue: existing.key
130
83
  });
131
84
 
132
- const model = await p.text({
85
+ const model = await text({
133
86
  message: "Claude Code 模型",
134
87
  initialValue: existing.model || undefined,
135
- validate: required()
88
+ validate: (value) => String(value || "").trim() ? undefined : "不能为空"
136
89
  });
137
90
  assertNotCancelled(model);
138
91
 
139
- const apiSettings = buildClaudeApiSettings(settings, {
92
+ await persistClaudeConfiguration(paths, {
140
93
  baseUrl,
141
94
  key: finalKey,
142
- model
143
- });
144
- const metaConfig = await readJsonFileSafe(paths.claudeMetaConfigPath, {}, { sensitive: true });
145
- metaConfig.hasCompletedOnboarding = true;
146
- ensureApprovedClaudeApiKey(metaConfig, finalKey);
147
- await persistClaudeConfiguration(paths, {
148
- settings: apiSettings,
149
- metaConfig
95
+ model: String(model).trim()
150
96
  });
151
97
 
152
- p.log.step(`已更新 ${pathToLabel(paths.claudeSettingsPath, paths.homeDir)} (${baseUrl}, ${maskSecret(finalKey)})`);
98
+ log.step(`已更新 ${pathToLabel(paths.claudeSettingsPath, paths.homeDir)} (${maskSecret(finalKey)})`);
153
99
  }
154
100
 
155
101
  export {
156
102
  buildClaudeApiSettings,
157
- buildDefaultClaudeSettings,
158
103
  configureClaudeApi,
159
- mergeClaudeSettingsWithDefaults,
160
104
  persistClaudeConfiguration
161
105
  };
@@ -2,128 +2,38 @@ import { lstat, mkdir, readFile, readdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import * as p from "@clack/prompts";
4
4
  import {
5
+ assertPlainObject,
6
+ assertSafeFilePath,
5
7
  pathExists,
6
8
  readJsonFileSafe,
7
- writeJson,
9
+ withFileTransaction,
8
10
  writeText
9
11
  } from "../config/store.mjs";
10
12
  import {
11
- buildTomlSection,
12
- detectLineEnding,
13
- extractTopLevelTomlEntries,
14
- getTomlSectionFieldNames,
15
13
  formatTomlKeySegment,
16
- mergeMissingTopLevelTomlEntries,
17
- parseTomlSection,
18
- readTopLevelTomlString,
19
- removeTomlSection,
14
+ parseToml,
20
15
  removeTomlSectionField,
21
16
  removeTopLevelTomlField,
22
17
  updateTomlSectionFields,
23
18
  updateTopLevelTomlField
24
19
  } from "../config/toml.mjs";
25
- import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
20
+ import { maskSecret, pathToLabel } from "../paths.mjs";
26
21
  import { hashBytes, isManagedCodexAgentFileEntry } from "../utils.mjs";
27
- import { normalizeOpenAiBaseUrl } from "./pi.mjs";
22
+ import { normalizeOpenAiBaseUrl } from "./url.mjs";
28
23
 
29
24
  const CODEX_ENV_KEY = "OPENAI_API_KEY";
25
+ const CODEX_PROVIDER_ID = "abelworkflow";
30
26
 
31
- const publishedCodexDeveloperInstructionHashes = new Set([
32
- "957adb227dd3fc298985acca38db4c22b8e018d387dab91f1ea8287db35385db"
33
- ]);
34
-
35
- const publishedCodexAgentHashes = {
36
- "default.toml": new Set([
37
- "7add720eb7ed030871088fe9f994a7e34a2b3cae1f898fe7f97c4e302f4a1844",
38
- "72d3da0275f4add64ff05da94f391443e8e4b5c43b646e0f89f2d17fa4345cb4",
39
- "7dc002559f5d78534e70c7c4683d51a48ffe94725c199855a35f61124aead876"
40
- ]),
41
- "explorer.toml": new Set([
42
- "e4e9c4eed4c028dded095b4cf36cb0ac408abcd86b9ab7c139440e1c58355644",
43
- "a236d7b586c4e312a62bb8b36115fe9780e28502c0af160ce5cd45b67c5b8479",
44
- "4740f13e9a478528dcb097188997445efa7607b1b07ae64774b1dd26fcd6f853"
45
- ]),
46
- "planner.toml": new Set([
47
- "2b16f821b14224cdfc9dab1fe14c41b9ff2b63642b0d9b340ef84b67a753a3b7",
48
- "741c9dfce5085d6bdc49f6e5004475974d54c2bde95c0b589eda86a63e96e58f",
49
- "7fde442a5ba5e996936b4ec853b036ebbb9f06ee9baef6540124d081c7107196"
50
- ]),
51
- "reviewer.toml": new Set([
52
- "08dcf8f5fee281f3b6120b63559086b7c675a619be72f761de4add711543b353",
53
- "9455d0189f324f7c606f7ae94ca338e9bd5c468f072188ac1f18e3749c918cd1",
54
- "a7292a533c9146bf5a2eb292cbfe889ca47bf4808324cddc1732c3c1721d437c"
55
- ]),
56
- "worker.toml": new Set([
57
- "13533715464b1f58a9c311e16dc3a29e92ecb95793fd8aff69a5ca731ae46ac8",
58
- "ea12b5b6173eb8fcaa57a8b447b1f81b35a6ac59e75025b88198c3f85175922c",
59
- "bfbc214e7b582774de759b118544c3f1fb61e1284d6ffd6a61ab772d28b6eaa0"
60
- ])
61
- };
62
-
63
- function stripCodexSubagentDefaults(content) {
64
- const lineEnding = detectLineEnding(content);
65
- let nextContent = removeTopLevelTomlField(content, "approvals_reviewer");
66
- nextContent = removeTopLevelTomlField(nextContent, "developer_instructions");
67
- nextContent = removeTomlSection(nextContent, "agents");
68
-
69
- const featureValues = parseTomlSection(nextContent, "features");
70
- delete featureValues.multi_agent;
71
- delete featureValues.guardian_approval;
72
- nextContent = removeTomlSection(nextContent, "features");
73
-
74
- if (Object.keys(featureValues).length) {
75
- const featuresSection = buildTomlSection("features", featureValues, lineEnding).trimEnd();
76
- nextContent = nextContent.trimEnd()
77
- ? `${nextContent.trimEnd()}${lineEnding}${lineEnding}${featuresSection}${lineEnding}`
78
- : `${featuresSection}${lineEnding}`;
27
+ function assertTomlTable(value, name) {
28
+ if (value === undefined) return {};
29
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
30
+ throw new Error(`${name} must be a TOML table`);
79
31
  }
80
-
81
- return nextContent;
82
- }
83
-
84
- function mergeCodexTemplateDefaults(content, templateContent) {
85
- const defaultTopLevelFields = new Set([
86
- "personality",
87
- "disable_response_storage",
88
- "approvals_reviewer",
89
- "approval_policy",
90
- "sandbox_mode",
91
- "service_tier",
92
- "model",
93
- "model_reasoning_effort",
94
- "developer_instructions"
95
- ]);
96
- const templateEntries = extractTopLevelTomlEntries(templateContent)
97
- .filter(({ field }) => defaultTopLevelFields.has(field));
98
- let nextContent = mergeMissingTopLevelTomlEntries(content, templateEntries);
99
- const currentDeveloperInstructions = readTopLevelTomlString(nextContent, "developer_instructions");
100
- const templateDeveloperInstructions = readTopLevelTomlString(templateContent, "developer_instructions");
101
- if (templateDeveloperInstructions && publishedCodexDeveloperInstructionHashes.has(
102
- hashBytes(currentDeveloperInstructions)
103
- )) {
104
- nextContent = updateTopLevelTomlField(
105
- nextContent,
106
- "developer_instructions",
107
- templateDeveloperInstructions
108
- );
32
+ const prototype = Object.getPrototypeOf(value);
33
+ if (prototype !== null && prototype !== Object.prototype) {
34
+ throw new Error(`${name} must be a TOML table`);
109
35
  }
110
-
111
- for (const sectionName of ["agents", "features"]) {
112
- const templateValues = parseTomlSection(templateContent, sectionName);
113
- const currentFields = new Set(getTomlSectionFieldNames(nextContent, sectionName));
114
- const missingValues = Object.fromEntries(
115
- Object.entries(templateValues).filter(([field]) => !currentFields.has(field))
116
- );
117
- if (Object.keys(missingValues).length) {
118
- nextContent = updateTomlSectionFields(nextContent, sectionName, missingValues);
119
- }
120
- }
121
-
122
- return nextContent;
123
- }
124
-
125
- async function loadBundledCodexConfigTemplate(paths = defaultPaths) {
126
- return readFile(paths.codexTemplateConfigPath, "utf8");
36
+ return value;
127
37
  }
128
38
 
129
39
  async function readCodexAgentTarget(path) {
@@ -172,11 +82,7 @@ async function ensureCodexAgentContainer(homeDir) {
172
82
  return targetDir;
173
83
  }
174
84
 
175
- function isPublishedCodexAgent(name, hash) {
176
- return publishedCodexAgentHashes[name]?.has(hash) ?? false;
177
- }
178
-
179
- async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFiles = {}) {
85
+ async function deployBundledCodexAgents(paths, previousManagedFiles = {}) {
180
86
  const targetDir = await ensureCodexAgentContainer(paths.homeDir);
181
87
  const entries = (await readdir(paths.codexTemplateAgentsPath, { withFileTypes: true }))
182
88
  .filter((entry) => entry.isFile() && entry.name.endsWith(".toml"))
@@ -203,10 +109,7 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
203
109
  result.created.push(target);
204
110
  } else if (currentHash === sourceHash) {
205
111
  result.unchanged.push(target);
206
- } else if (current.content && (
207
- previousManagedFiles?.[name] === currentHash
208
- || isPublishedCodexAgent(name, currentHash)
209
- )) {
112
+ } else if (current.content && previousManagedFiles?.[name] === currentHash) {
210
113
  await writeText(target, sourceContent, { backupLimit: 0 });
211
114
  result.updated.push(target);
212
115
  } else {
@@ -240,10 +143,6 @@ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFil
240
143
  return result;
241
144
  }
242
145
 
243
- function getCodexProviderSectionName(providerId) {
244
- return `model_providers.${formatTomlKeySegment(providerId)}`;
245
- }
246
-
247
146
  function assertCodexAuthKeyAvailable(auth, envKey) {
248
147
  if (
249
148
  auth
@@ -257,46 +156,80 @@ function assertCodexAuthKeyAvailable(auth, envKey) {
257
156
  }
258
157
 
259
158
  function resolveExistingCodexApiConfig(content, auth = {}) {
260
- const providerId = readTopLevelTomlString(content, "model_provider") || "abelworkflow";
261
- const provider = parseTomlSection(content, getCodexProviderSectionName(providerId));
159
+ const config = parseToml(content);
160
+ assertPlainObject(auth, "Codex auth");
161
+ const providers = assertTomlTable(config.model_providers, "model_providers");
162
+ const provider = assertTomlTable(
163
+ providers[CODEX_PROVIDER_ID],
164
+ `model_providers.${CODEX_PROVIDER_ID}`
165
+ );
262
166
  const envKey = CODEX_ENV_KEY;
263
167
  assertCodexAuthKeyAvailable(auth, envKey);
264
168
  const apiKey = typeof auth[envKey] === "string" ? auth[envKey] : "";
265
169
 
266
170
  return {
267
- providerId,
268
- providerName: provider.name || providerId,
269
- baseUrl: provider.base_url || "https://api.openai.com/v1",
171
+ providerId: CODEX_PROVIDER_ID,
172
+ providerName: typeof provider.name === "string" && provider.name ? provider.name : CODEX_PROVIDER_ID,
173
+ baseUrl: typeof provider.base_url === "string" && provider.base_url
174
+ ? provider.base_url
175
+ : "https://api.openai.com/v1",
270
176
  envKey,
271
177
  apiKey
272
178
  };
273
179
  }
274
180
 
275
- async function getExistingCodexApiConfig(paths = defaultPaths) {
181
+ async function getExistingCodexApiConfig(paths) {
182
+ await assertSafeFilePath(paths.codexConfigPath, paths.homeDir);
276
183
  const content = await pathExists(paths.codexConfigPath) ? await readFile(paths.codexConfigPath, "utf8") : "";
277
- const auth = await readJsonFileSafe(paths.codexAuthPath, {}, { sensitive: true });
184
+ const auth = await readJsonFileSafe(paths.codexAuthPath, {}, {
185
+ safeRoot: paths.homeDir,
186
+ sensitive: true
187
+ });
278
188
  return resolveExistingCodexApiConfig(content, auth);
279
189
  }
280
190
 
281
- async function persistCodexConfiguration(paths, { content, auth }) {
282
- await writeText(paths.codexConfigPath, content);
283
- await writeJson(paths.codexAuthPath, auth, { sensitive: true });
191
+ async function persistCodexConfiguration(paths, intent, operations = {}) {
192
+ return withFileTransaction([
193
+ { path: paths.codexConfigPath, safeRoot: paths.homeDir },
194
+ { path: paths.codexAuthPath, safeRoot: paths.homeDir, sensitive: true }
195
+ ], async (transaction) => {
196
+ const currentContent = transaction.readText(paths.codexConfigPath);
197
+ const currentAuth = transaction.readJson(paths.codexAuthPath, {});
198
+ assertPlainObject(currentAuth, "Codex auth");
199
+ const current = resolveExistingCodexApiConfig(currentContent, currentAuth);
200
+ const content = buildCodexConfigContent(currentContent, {
201
+ providerName: current.providerName,
202
+ baseUrl: intent.baseUrl
203
+ });
204
+ const auth = mergeCodexAuthData(currentAuth, intent.apiKey);
205
+ await transaction.writeText(paths.codexConfigPath, content, {
206
+ ...(operations.writeConfig ? { writer: operations.writeConfig } : {})
207
+ });
208
+ await transaction.writeJson(paths.codexAuthPath, auth, {
209
+ ...(operations.writeAuth ? { writer: operations.writeAuth } : {})
210
+ });
211
+ });
284
212
  }
285
213
 
286
- async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}) {
214
+ async function configureCodexApi(paths, promptApi, ownership = {}, runtime = {}) {
215
+ const text = runtime.text ?? p.text;
216
+ const log = runtime.log ?? p.log;
287
217
  const {
288
218
  assertNotCancelled,
289
219
  confirmOrCancel,
290
- passwordOrExisting,
291
- required
220
+ passwordOrExisting
292
221
  } = promptApi;
293
222
  const existing = await getExistingCodexApiConfig(paths);
294
- const providerId = existing.providerId || "abelworkflow";
295
- const providerName = existing.providerName || providerId;
296
- const baseUrlInput = await p.text({
223
+ const baseUrlInput = await text({
297
224
  message: "Codex Base URL",
298
225
  initialValue: existing.baseUrl,
299
- validate: required()
226
+ validate: (value) => {
227
+ try {
228
+ normalizeOpenAiBaseUrl(value);
229
+ } catch (error) {
230
+ return error.message;
231
+ }
232
+ }
300
233
  });
301
234
  assertNotCancelled(baseUrlInput);
302
235
  const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
@@ -309,97 +242,69 @@ async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}
309
242
  const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
310
243
  let managedCodexAgentFiles = { ...(ownership.managedCodexAgentFiles ?? {}) };
311
244
 
312
- const envKey = CODEX_ENV_KEY;
313
- const currentContent = await pathExists(paths.codexConfigPath) ? await readFile(paths.codexConfigPath, "utf8") : "";
314
- const templateContent = await loadBundledCodexConfigTemplate(paths);
315
- const content = buildCodexConfigContent(currentContent, {
316
- templateContent,
317
- mergeMissingTemplateDefaults: true,
318
- includeSubagentDefaults: shouldDeploySubagents,
319
- providerId,
320
- providerName,
321
- baseUrl
322
- });
323
-
324
- const auth = mergeCodexAuthData(
325
- await readJsonFileSafe(paths.codexAuthPath, {}, { sensitive: true }),
326
- envKey,
327
- finalApiKey,
328
- ownership.managedAuthKeys ?? []
329
- );
330
- await persistCodexConfiguration(paths, { content, auth });
245
+ await ownership.guard?.();
246
+ await persistCodexConfiguration(paths, { baseUrl, apiKey: finalApiKey });
331
247
 
332
- p.log.step(`已更新 ${pathToLabel(paths.codexConfigPath, paths.homeDir)} (${providerId}, ${baseUrl})`);
333
- p.log.step(`已更新 ${pathToLabel(paths.codexAuthPath, paths.homeDir)} (${maskSecret(finalApiKey)})`);
248
+ log.step(`已更新 ${pathToLabel(paths.codexConfigPath, paths.homeDir)} (${CODEX_PROVIDER_ID})`);
249
+ log.step(`已更新 ${pathToLabel(paths.codexAuthPath, paths.homeDir)} (${maskSecret(finalApiKey)})`);
334
250
  if (String(baseUrl).startsWith("https://") && !process.env.CODEX_CA_CERTIFICATE && !process.env.SSL_CERT_FILE) {
335
- p.log.message("自签名证书需在启动 Codex 前设置 CODEX_CA_CERTIFICATE=/absolute/path/relay-ca.pem。");
251
+ log.message("自签名证书需在启动 Codex 前设置 CODEX_CA_CERTIFICATE=/absolute/path/relay-ca.pem。");
336
252
  }
337
253
  if (shouldDeploySubagents) {
254
+ await ownership.guard?.();
338
255
  const deployment = await deployBundledCodexAgents(paths, managedCodexAgentFiles);
339
256
  managedCodexAgentFiles = deployment.managedFiles;
340
- p.log.step(`Codex subagents 同步完成:新增 ${deployment.created.length},更新 ${deployment.updated.length},未变 ${deployment.unchanged.length}`);
257
+ log.step(`Codex subagents 同步完成:新增 ${deployment.created.length},更新 ${deployment.updated.length},未变 ${deployment.unchanged.length}`);
341
258
  if (deployment.conflicts.length) {
342
- p.log.warn([
259
+ log.warn([
343
260
  `检测到 ${deployment.conflicts.length} 个 Codex subagent 冲突,已保留原文件:`,
344
261
  ...deployment.conflicts.map((path) => `- ${pathToLabel(path, paths.homeDir)}`)
345
262
  ].join("\n"));
346
263
  }
347
264
  } else {
348
- p.log.message("已跳过 Codex subagents 部署。");
265
+ log.message("已跳过 Codex subagents 部署。");
349
266
  }
350
267
  return {
351
- managedAuthKeys: finalApiKey ? [envKey] : [],
352
268
  managedCodexAgentFiles
353
269
  };
354
270
  }
355
271
 
356
272
  function buildCodexConfigContent(currentContent, {
357
- templateContent = "",
358
- mergeMissingTemplateDefaults = false,
359
- includeSubagentDefaults = true,
360
- providerId,
361
273
  providerName,
362
274
  baseUrl
363
275
  }) {
364
- const effectiveTemplateContent = includeSubagentDefaults
365
- ? templateContent
366
- : stripCodexSubagentDefaults(templateContent);
367
- const hasCurrentContent = Boolean(currentContent.trim());
368
- let content = hasCurrentContent ? currentContent : effectiveTemplateContent;
369
- if (hasCurrentContent && mergeMissingTemplateDefaults && effectiveTemplateContent.trim()) {
370
- content = mergeCodexTemplateDefaults(content, effectiveTemplateContent);
371
- }
372
- const lineEnding = detectLineEnding(content);
373
- if (includeSubagentDefaults && readTopLevelTomlString(content, "approvals_reviewer") === "reviewer") {
374
- content = updateTopLevelTomlField(content, "approvals_reviewer", "guardian_subagent");
375
- }
376
- const providerSectionName = getCodexProviderSectionName(providerId);
377
- content = updateTopLevelTomlField(content, "model_provider", providerId);
276
+ const config = parseToml(currentContent);
277
+ const providers = assertTomlTable(config.model_providers, "model_providers");
278
+ const provider = assertTomlTable(
279
+ providers[CODEX_PROVIDER_ID],
280
+ `model_providers.${CODEX_PROVIDER_ID}`
281
+ );
282
+ const providerSection = `model_providers.${formatTomlKeySegment(CODEX_PROVIDER_ID)}`;
283
+ let content = updateTopLevelTomlField(currentContent, "model_provider", CODEX_PROVIDER_ID);
378
284
  content = removeTopLevelTomlField(content, "preferred_auth_method");
379
285
  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, {
383
- name: providerName,
384
- base_url: baseUrl,
286
+ for (const field of ["env_key", "temp_env_key", "supports_websockets"]) {
287
+ content = removeTomlSectionField(content, providerSection, field);
288
+ }
289
+ content = updateTomlSectionFields(content, providerSection, {
290
+ name: providerName || CODEX_PROVIDER_ID,
291
+ base_url: normalizeOpenAiBaseUrl(baseUrl),
385
292
  wire_api: "responses",
386
- requires_openai_auth: true,
387
- supports_websockets: true
293
+ requires_openai_auth: true
388
294
  });
389
- return `${content.trim()}${lineEnding}`;
295
+ parseToml(content);
296
+ return content;
390
297
  }
391
298
 
392
- function mergeCodexAuthData(auth, envKey, apiKey, managedAuthKeys = []) {
393
- const nextAuth = auth && typeof auth === "object" && !Array.isArray(auth) ? { ...auth } : {};
394
- assertCodexAuthKeyAvailable(nextAuth, envKey);
395
- for (const managedAuthKey of managedAuthKeys) {
396
- delete nextAuth[managedAuthKey];
397
- }
299
+ function mergeCodexAuthData(auth, apiKey) {
300
+ assertPlainObject(auth, "Codex auth");
301
+ const nextAuth = { ...auth };
302
+ assertCodexAuthKeyAvailable(nextAuth, CODEX_ENV_KEY);
398
303
  if (apiKey) {
399
304
  nextAuth.auth_mode = "apikey";
400
- nextAuth[envKey] = apiKey;
305
+ nextAuth[CODEX_ENV_KEY] = apiKey;
401
306
  } else {
402
- delete nextAuth[envKey];
307
+ delete nextAuth[CODEX_ENV_KEY];
403
308
  if (nextAuth.auth_mode === "apikey") delete nextAuth.auth_mode;
404
309
  }
405
310
  return nextAuth;
@@ -410,8 +315,6 @@ export {
410
315
  configureCodexApi,
411
316
  deployBundledCodexAgents,
412
317
  mergeCodexAuthData,
413
- mergeCodexTemplateDefaults,
414
318
  persistCodexConfiguration,
415
- resolveExistingCodexApiConfig,
416
- stripCodexSubagentDefaults
319
+ resolveExistingCodexApiConfig
417
320
  };