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