helloagents 3.0.18 → 3.0.19-beta.1

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.
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ import { ensureTimestampedBackup } from './cli-codex-backup.mjs'
8
+ import {
9
+ CODEX_GOALS_FEATURE_KEY,
10
+ CODEX_MANAGED_TOML_COMMENT,
11
+ isManagedCodexGoalsFeature,
12
+ readCodexGoalsFeatureLine,
13
+ removeCodexGoalsFeatureConfig,
14
+ setCodexGoalsFeatureConfig,
15
+ } from './cli-codex-config.mjs'
16
+ import { ensureDir, safeRead, safeWrite } from './cli-utils.mjs'
17
+
18
+ const CODEX_CONFIG_BASENAME = 'config.toml'
19
+
20
+ function isJsonMode(args) {
21
+ return args.includes('--json')
22
+ }
23
+
24
+ function readStatus(home) {
25
+ const configPath = join(home, '.codex', CODEX_CONFIG_BASENAME)
26
+ const toml = safeRead(configPath) || ''
27
+ const line = readCodexGoalsFeatureLine(toml)
28
+ const valueMatch = line.match(/^\s*goals\s*=\s*(true|false)\b/)
29
+ const configured = Boolean(valueMatch)
30
+ const enabled = valueMatch ? valueMatch[1] === 'true' : false
31
+
32
+ return {
33
+ configPath,
34
+ configured,
35
+ enabled,
36
+ managed: isManagedCodexGoalsFeature(line),
37
+ line,
38
+ }
39
+ }
40
+
41
+ function printStatus(status, jsonMode) {
42
+ if (jsonMode) {
43
+ process.stdout.write(`${JSON.stringify({
44
+ feature: CODEX_GOALS_FEATURE_KEY,
45
+ enabled: status.enabled,
46
+ configured: status.configured,
47
+ managed: status.managed,
48
+ configPath: status.configPath,
49
+ }, null, 2)}\n`)
50
+ return
51
+ }
52
+
53
+ const state = status.enabled ? 'enabled' : 'disabled'
54
+ const source = status.configured ? (status.managed ? 'managed' : 'user') : 'default'
55
+ process.stdout.write(`Codex goals: ${state} (${source})\n`)
56
+ }
57
+
58
+ function updateGoals(home, enabled) {
59
+ const codexDir = join(home, '.codex')
60
+ const configPath = join(codexDir, CODEX_CONFIG_BASENAME)
61
+ ensureDir(codexDir)
62
+ ensureTimestampedBackup(configPath, CODEX_CONFIG_BASENAME)
63
+
64
+ const current = safeRead(configPath) || ''
65
+ const next = enabled
66
+ ? setCodexGoalsFeatureConfig(current, true)
67
+ : removeCodexGoalsFeatureConfig(setCodexGoalsFeatureConfig(current, false))
68
+
69
+ if (next.trim()) safeWrite(configPath, next)
70
+ else safeWrite(configPath, '')
71
+ }
72
+
73
+ function printUsage() {
74
+ process.stdout.write([
75
+ 'Usage: helloagents codex goals <status|enable|disable> [--json]',
76
+ '',
77
+ `This only manages [features].goals ${CODEX_MANAGED_TOML_COMMENT}.`,
78
+ ].join('\n') + '\n')
79
+ }
80
+
81
+ export function runCodexGoalsCli(args = process.argv.slice(2), { home = homedir() } = {}) {
82
+ const command = args.find((arg) => !arg.startsWith('--')) || 'status'
83
+ const jsonMode = isJsonMode(args)
84
+
85
+ if (!existsSync(join(home, '.codex')) && command === 'status') {
86
+ const status = {
87
+ configPath: join(home, '.codex', CODEX_CONFIG_BASENAME),
88
+ configured: false,
89
+ enabled: false,
90
+ managed: false,
91
+ line: '',
92
+ }
93
+ printStatus(status, jsonMode)
94
+ return
95
+ }
96
+
97
+ if (command === 'status') {
98
+ printStatus(readStatus(home), jsonMode)
99
+ return
100
+ }
101
+
102
+ if (command === 'enable' || command === 'disable') {
103
+ updateGoals(home, command === 'enable')
104
+ printStatus(readStatus(home), jsonMode)
105
+ return
106
+ }
107
+
108
+ printUsage()
109
+ process.exitCode = 1
110
+ }
111
+
112
+ if (process.argv[1]?.endsWith('cli-codex-goals.mjs')) {
113
+ runCodexGoalsCli()
114
+ }
@@ -4,6 +4,7 @@ import {
4
4
  ensureDir, safeRead, safeWrite, removeIfExists,
5
5
  readJsonOrThrow, copyEntries,
6
6
  createLink, removeLink, injectMarkedContent, removeMarkedContent,
7
+ cleanSettingsHooks, loadHooksWithCliEntry, mergeSettingsHooks,
7
8
  } from './cli-utils.mjs';
8
9
  import { ensureTimestampedBackup, readCodexBackup, removeCodexBackup } from './cli-codex-backup.mjs';
9
10
  import {
@@ -12,9 +13,16 @@ import {
12
13
  CODEX_PLUGIN_CONFIG_HEADER,
13
14
  installCodexManagedTopLevelConfig,
14
15
  isManagedCodexBackupInstruction,
16
+ isManagedCodexGoalsFeature,
15
17
  isManagedCodexModelInstruction,
16
18
  isManagedCodexNotify,
19
+ isManagedLegacyCodexHooksFeature,
20
+ readCodexGoalsFeatureLine,
21
+ readLegacyCodexHooksFeatureLine,
22
+ removeCodexGoalsFeatureConfig,
23
+ removeLegacyManagedCodexHooksFeatureConfig,
17
24
  removeCodexPluginConfig,
25
+ restoreCodexGoalsFeatureConfig,
18
26
  restoreCodexTopLevelConfig,
19
27
  upsertCodexPluginConfig,
20
28
  } from './cli-codex-config.mjs';
@@ -31,6 +39,7 @@ export const CODEX_PLUGIN_KEY = `${CODEX_PLUGIN_NAME}@${CODEX_MARKETPLACE_NAME}`
31
39
  export { CODEX_MANAGED_TOML_COMMENT, CODEX_PLUGIN_CONFIG_HEADER };
32
40
  export const CODEX_RUNTIME_CARRIER = 'AGENTS.md';
33
41
  const CODEX_CONFIG_BASENAME = 'config.toml';
42
+ const CODEX_HOOKS_BASENAME = 'hooks.json';
34
43
  export const CODEX_RUNTIME_ENTRIES = [
35
44
  '.codex-plugin',
36
45
  'assets',
@@ -120,19 +129,40 @@ function writeCodexRuntimeCarrier(filePath, bootstrapPath, settings) {
120
129
  return true;
121
130
  }
122
131
 
132
+ function installCodexStandaloneHooks(home, pkgRoot) {
133
+ const hooksData = loadHooksWithCliEntry(pkgRoot, 'hooks-codex.json', '${PLUGIN_ROOT}');
134
+ if (!hooksData) return false;
135
+ mergeSettingsHooks(join(home, '.codex', CODEX_HOOKS_BASENAME), hooksData);
136
+ return true;
137
+ }
138
+
139
+ function cleanupCodexStandaloneHooks(home) {
140
+ cleanSettingsHooks(join(home, '.codex', CODEX_HOOKS_BASENAME));
141
+ }
142
+
123
143
  function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } = {}) {
124
144
  const backupToml = readCodexBackup(configPath, CODEX_CONFIG_BASENAME);
125
145
  let toml = safeRead(configPath) || '';
126
146
 
127
147
  const currentModelInstructions = readTopLevelTomlLine(toml, 'model_instructions_file');
128
148
  const currentNotify = readTopLevelTomlBlock(toml, 'notify');
149
+ const currentCodexGoalsFeature = readCodexGoalsFeatureLine(toml);
150
+ const currentLegacyCodexHooksFeature = readLegacyCodexHooksFeatureLine(toml);
129
151
 
130
152
  const shouldRestoreModelInstructions = isManagedCodexModelInstruction(currentModelInstructions);
131
153
  const shouldRestoreNotify = isManagedCodexNotify(currentNotify);
154
+ const shouldRestoreCodexGoalsFeature = isManagedCodexGoalsFeature(currentCodexGoalsFeature);
155
+ const shouldRemoveLegacyCodexHooksFeature = isManagedLegacyCodexHooksFeature(currentLegacyCodexHooksFeature);
132
156
 
133
157
  if (removePluginConfig) {
134
158
  toml = removeCodexPluginConfig(toml);
135
159
  }
160
+ if (shouldRestoreCodexGoalsFeature) {
161
+ toml = removeCodexGoalsFeatureConfig(toml);
162
+ }
163
+ if (shouldRemoveLegacyCodexHooksFeature) {
164
+ toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
165
+ }
136
166
  if (shouldRestoreModelInstructions) {
137
167
  toml = removeTopLevelTomlLines(toml, (line) =>
138
168
  line.startsWith('model_instructions_file =') && isManagedCodexModelInstruction(line)).text;
@@ -144,6 +174,7 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
144
174
 
145
175
  const backupModelInstructions = readTopLevelTomlLine(backupToml, 'model_instructions_file');
146
176
  const backupNotify = readTopLevelTomlBlock(backupToml, 'notify');
177
+ const backupCodexGoalsFeature = readCodexGoalsFeatureLine(backupToml);
147
178
 
148
179
  toml = restoreCodexTopLevelConfig(toml, {
149
180
  modelInstructionsLine: shouldRestoreModelInstructions && !isManagedCodexBackupInstruction(backupModelInstructions)
@@ -153,6 +184,11 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
153
184
  ? backupNotify
154
185
  : '',
155
186
  });
187
+ toml = restoreCodexGoalsFeatureConfig(toml, {
188
+ codexGoalsLine: shouldRestoreCodexGoalsFeature && !isManagedCodexGoalsFeature(backupCodexGoalsFeature)
189
+ ? backupCodexGoalsFeature
190
+ : '',
191
+ });
156
192
 
157
193
  return toml;
158
194
  }
@@ -173,7 +209,9 @@ export function installCodexStandby(home, pkgRoot) {
173
209
  toml = installCodexManagedTopLevelConfig(toml, {
174
210
  modelInstructionsPath: CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
175
211
  });
212
+ toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
176
213
  safeWrite(configPath, toml);
214
+ installCodexStandaloneHooks(home, pkgRoot);
177
215
 
178
216
  createLink(pkgRoot, join(codexDir, 'helloagents'));
179
217
  return true;
@@ -210,6 +248,7 @@ export function uninstallCodexStandby(home) {
210
248
  else removeIfExists(configPath);
211
249
  changed = true;
212
250
  removeCodexBackup(configPath, CODEX_CONFIG_BASENAME);
251
+ cleanupCodexStandaloneHooks(home);
213
252
  removeLink(join(codexDir, 'helloagents'));
214
253
  changed = true;
215
254
  }
@@ -265,8 +304,10 @@ export function installCodexGlobal(home, pkgRoot) {
265
304
  toml = installCodexManagedTopLevelConfig(toml, {
266
305
  modelInstructionsPath: CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
267
306
  });
307
+ toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
268
308
  toml = upsertCodexPluginConfig(toml);
269
309
  safeWrite(configPath, toml);
310
+ installCodexStandaloneHooks(home, pkgRoot);
270
311
 
271
312
  return true;
272
313
  }
@@ -284,6 +325,7 @@ export function uninstallCodexGlobal(home) {
284
325
  removeCodexMarketplaceEntry(marketplaceFile);
285
326
  removeMarkedContent(join(codexDir, 'AGENTS.md'));
286
327
  removeLink(join(codexDir, 'helloagents'));
328
+ cleanupCodexStandaloneHooks(home);
287
329
 
288
330
  const toml = cleanupCodexManagedConfig(configPath, { removePluginConfig: true });
289
331
  if (toml.trim()) safeWrite(configPath, toml);
@@ -0,0 +1,208 @@
1
+ import { existsSync, realpathSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import { CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_CONFIG_HEADER, CODEX_PLUGIN_NAME } from './cli-codex.mjs'
5
+ import {
6
+ CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
7
+ CODEX_MANAGED_NOTIFY_VALUE,
8
+ readCodexGoalsFeatureLine,
9
+ readCodexHooksFeatureLine,
10
+ readLegacyCodexHooksFeatureLine,
11
+ } from './cli-codex-config.mjs'
12
+ import { buildRuntimeCarrier } from './cli-runtime-carrier.mjs'
13
+ import { readTopLevelTomlLine } from './cli-toml.mjs'
14
+ import { loadHooksWithCliEntry, safeJson, safeRead } from './cli-utils.mjs'
15
+
16
+ function safeRealTarget(linkPath) {
17
+ try {
18
+ return realpathSync(linkPath)
19
+ } catch {
20
+ return ''
21
+ }
22
+ }
23
+
24
+ function normalizeText(text = '') {
25
+ return String(text || '').replace(/\r\n/g, '\n').trim()
26
+ }
27
+
28
+ function normalizePath(value = '') {
29
+ return String(value || '').replace(/\\/g, '/')
30
+ }
31
+
32
+ function sortJson(value) {
33
+ if (Array.isArray(value)) return value.map(sortJson)
34
+ if (value && typeof value === 'object') {
35
+ return Object.keys(value).sort().reduce((acc, key) => {
36
+ acc[key] = sortJson(value[key])
37
+ return acc
38
+ }, {})
39
+ }
40
+ return value
41
+ }
42
+
43
+ function pickManagedHooks(hooks) {
44
+ const next = {}
45
+ for (const [event, entries] of Object.entries(hooks || {})) {
46
+ if (!Array.isArray(entries)) continue
47
+ const managedEntries = entries.filter((entry) => JSON.stringify(entry).includes('helloagents'))
48
+ if (managedEntries.length > 0) next[event] = managedEntries
49
+ }
50
+ return next
51
+ }
52
+
53
+ function managedHooksMatch(actualHooks, expectedHooks) {
54
+ return JSON.stringify(sortJson(pickManagedHooks(actualHooks || {})))
55
+ === JSON.stringify(sortJson(expectedHooks || {}))
56
+ }
57
+
58
+ function readExpectedHooks(runtime, hooksFile, pathVar) {
59
+ return pickManagedHooks(loadHooksWithCliEntry(runtime.pkgRoot, hooksFile, pathVar)?.hooks || {})
60
+ }
61
+
62
+ function readExpectedCarrierContent(runtime, fileName, settings) {
63
+ const bootstrap = safeRead(join(runtime.pkgRoot, fileName)) || ''
64
+ return normalizeText(buildRuntimeCarrier(bootstrap, settings))
65
+ }
66
+
67
+ function buildDoctorIssue(runtime, code, cn, en) {
68
+ return {
69
+ code,
70
+ message: runtime.msg(cn, en),
71
+ }
72
+ }
73
+
74
+ function normalizeDoctorMode(mode = '') {
75
+ return mode || 'none'
76
+ }
77
+
78
+ function summarizeDoctorStatus(issues, { trackedMode, detectedMode } = {}) {
79
+ if (issues.length > 0) return 'drift'
80
+ if (detectedMode !== 'none') return 'ok'
81
+ if (trackedMode !== 'none') return 'drift'
82
+ return 'not-installed'
83
+ }
84
+
85
+ function suggestCodexDoctorFix(status, trackedMode) {
86
+ if (status === 'drift') {
87
+ return `helloagents update codex${trackedMode && trackedMode !== 'none' ? ` --${trackedMode}` : ''}`
88
+ }
89
+ if (status === 'not-installed') return 'helloagents install codex --standby'
90
+ return ''
91
+ }
92
+
93
+ function appendCodexStandbyIssues(runtime, issues, checks) {
94
+ if (!checks.carrierMarker) issues.push(buildDoctorIssue(runtime, 'standby-carrier-missing', 'standby `~/.codex/AGENTS.md` 缺少 HelloAGENTS 标记', 'Standby `~/.codex/AGENTS.md` is missing the HELLOAGENTS marker'))
95
+ if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue(runtime, 'standby-carrier-drift', 'standby `~/.codex/AGENTS.md` 与当前 bootstrap-lite.md 不一致', 'Standby `~/.codex/AGENTS.md` differs from the current bootstrap-lite.md'))
96
+ if (!checks.homeLink) issues.push(buildDoctorIssue(runtime, 'standby-link-missing', 'standby `~/.codex/helloagents` 链接缺失或未指向稳定运行根目录', 'Standby `~/.codex/helloagents` link is missing or points to a different runtime root'))
97
+ if (!checks.modelInstructionsFile) issues.push(buildDoctorIssue(runtime, 'standby-model-instructions-missing', 'standby config 缺少受管 model_instructions_file', 'Standby config is missing the managed model_instructions_file'))
98
+ if (checks.modelInstructionsFile && !checks.modelInstructionsPathMatch) issues.push(buildDoctorIssue(runtime, 'standby-model-instructions-drift', 'standby model_instructions_file 未指向受管 `~/.codex/AGENTS.md`', 'Standby model_instructions_file does not point to the managed `~/.codex/AGENTS.md`'))
99
+ if (!checks.codexNotify) issues.push(buildDoctorIssue(runtime, 'standby-notify-missing', 'standby notify 配置缺失', 'Standby notify configuration is missing'))
100
+ if (checks.codexNotify && !checks.notifyPathMatch) issues.push(buildDoctorIssue(runtime, 'standby-notify-drift', 'standby notify 未使用受管命令入口', 'Standby notify does not use the managed command entrypoint'))
101
+ if (!checks.codexHooksFeature) issues.push(buildDoctorIssue(runtime, 'codex-hooks-feature-disabled', 'Codex hooks 功能被显式关闭', 'Codex hooks feature is explicitly disabled'))
102
+ if (!checks.standaloneHooks) issues.push(buildDoctorIssue(runtime, 'standby-hooks-missing', 'standby `~/.codex/hooks.json` 缺少 HelloAGENTS hooks', 'Standby `~/.codex/hooks.json` is missing HelloAGENTS hooks'))
103
+ if (checks.standaloneHooks && !checks.standaloneHooksMatch) issues.push(buildDoctorIssue(runtime, 'standby-hooks-drift', 'standby `~/.codex/hooks.json` 与当前 hooks-codex.json 不一致', 'Standby `~/.codex/hooks.json` differs from the current hooks-codex.json'))
104
+ if (checks.pluginRoot || checks.pluginCache || checks.marketplaceEntry || checks.pluginEnabled) {
105
+ issues.push(buildDoctorIssue(runtime, 'standby-global-residue', 'standby 模式下仍残留 global 插件文件或配置', 'Global plugin artifacts still remain while Codex is in standby mode'))
106
+ }
107
+ }
108
+
109
+ function appendCodexGlobalIssues(runtime, issues, checks, pluginVersion, cacheVersion) {
110
+ if (!checks.carrierMarker) issues.push(buildDoctorIssue(runtime, 'global-home-carrier-missing', 'global `~/.codex/AGENTS.md` 缺少 HelloAGENTS 规则内容', 'Global `~/.codex/AGENTS.md` is missing the HelloAGENTS carrier'))
111
+ if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue(runtime, 'global-home-carrier-drift', 'global `~/.codex/AGENTS.md` 与当前 bootstrap.md 不一致', 'Global `~/.codex/AGENTS.md` differs from the current bootstrap.md'))
112
+ if (!checks.globalHomeLink) issues.push(buildDoctorIssue(runtime, 'global-read-root-link-missing', 'global `~/.codex/helloagents` 链接缺失或未指向当前插件根目录', 'Global `~/.codex/helloagents` link is missing or does not point to the current plugin root'))
113
+ if (!checks.pluginRoot) issues.push(buildDoctorIssue(runtime, 'global-plugin-root-missing', 'global 插件根目录缺失', 'Global plugin root is missing'))
114
+ if (!checks.pluginCache) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-missing', 'global 插件缓存目录缺失', 'Global plugin cache directory is missing'))
115
+ if (checks.pluginRoot && !checks.pluginCarrierMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-carrier-drift', 'global 插件根目录中的 AGENTS.md 与当前 bootstrap.md 不一致', 'Global plugin AGENTS.md differs from the current bootstrap.md'))
116
+ if (checks.pluginCache && !checks.pluginCacheCarrierMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-carrier-drift', 'global 插件缓存中的 AGENTS.md 与当前 bootstrap.md 不一致', 'Global plugin cache AGENTS.md differs from the current bootstrap.md'))
117
+ if (!checks.marketplaceEntry) issues.push(buildDoctorIssue(runtime, 'global-marketplace-missing', 'global marketplace 条目缺失', 'Global marketplace entry is missing'))
118
+ if (!checks.pluginEnabled) issues.push(buildDoctorIssue(runtime, 'global-plugin-disabled', 'global config 中缺少插件启用段', 'Global plugin enablement block is missing from config'))
119
+ if (!checks.modelInstructionsFile) issues.push(buildDoctorIssue(runtime, 'global-model-instructions-missing', 'global config 缺少受管 model_instructions_file', 'Global config is missing the managed model_instructions_file'))
120
+ if (checks.modelInstructionsFile && !checks.modelInstructionsPathMatch) issues.push(buildDoctorIssue(runtime, 'global-model-instructions-drift', 'global model_instructions_file 未指向受管 `~/.codex/AGENTS.md`', 'Global model_instructions_file does not point to the managed `~/.codex/AGENTS.md`'))
121
+ if (!checks.codexNotify) issues.push(buildDoctorIssue(runtime, 'global-notify-missing', 'global notify 配置缺失', 'Global notify configuration is missing'))
122
+ if (checks.codexNotify && !checks.globalNotifyPathMatch) issues.push(buildDoctorIssue(runtime, 'global-notify-drift', 'global notify 未使用受管命令入口', 'Global notify does not use the managed command entrypoint'))
123
+ if (!checks.codexHooksFeature) issues.push(buildDoctorIssue(runtime, 'codex-hooks-feature-disabled', 'Codex hooks 功能被显式关闭', 'Codex hooks feature is explicitly disabled'))
124
+ if (!checks.standaloneHooks) issues.push(buildDoctorIssue(runtime, 'global-hooks-missing', 'global `~/.codex/hooks.json` 缺少 HelloAGENTS hooks', 'Global `~/.codex/hooks.json` is missing HelloAGENTS hooks'))
125
+ if (checks.standaloneHooks && !checks.standaloneHooksMatch) issues.push(buildDoctorIssue(runtime, 'global-hooks-drift', 'global `~/.codex/hooks.json` 与当前 hooks-codex.json 不一致', 'Global `~/.codex/hooks.json` differs from the current hooks-codex.json'))
126
+ if (pluginVersion && !checks.pluginVersionMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-version-drift', 'global 插件根目录版本与当前包版本不一致', 'Global plugin root version does not match the current package version'))
127
+ if (cacheVersion && !checks.pluginCacheVersionMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-version-drift', 'global 插件缓存版本与当前包版本不一致', 'Global plugin cache version does not match the current package version'))
128
+ if (checks.homeLink) issues.push(buildDoctorIssue(runtime, 'global-standby-link-residue', 'global 模式下仍残留 standby home 链接', 'Standby home link still remains while Codex is in global mode'))
129
+ }
130
+
131
+ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
132
+ const codexDir = join(runtime.home, '.codex')
133
+ const codexConfig = safeRead(join(codexDir, 'config.toml')) || ''
134
+ const pluginRoot = join(runtime.home, 'plugins', CODEX_PLUGIN_NAME)
135
+ const pluginCacheRoot = join(codexDir, 'plugins', 'cache', CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME, 'local')
136
+ const homeLinkTarget = safeRealTarget(join(codexDir, 'helloagents'))
137
+ const expectedHomeCarrier = (detectedMode === 'global' || (detectedMode === 'none' && trackedMode === 'global'))
138
+ ? 'bootstrap.md'
139
+ : 'bootstrap-lite.md'
140
+ const codexHooks = safeJson(join(codexDir, 'hooks.json')) || {}
141
+ const marketplace = safeJson(join(runtime.home, '.agents', 'plugins', 'marketplace.json')) || {}
142
+ const modelInstructionsLine = readTopLevelTomlLine(codexConfig, 'model_instructions_file')
143
+ const expectedHooks = readExpectedHooks(runtime, 'hooks-codex.json', '${PLUGIN_ROOT}')
144
+ const hooksFeatureLine = readCodexHooksFeatureLine(codexConfig)
145
+ const goalsFeatureLine = readCodexGoalsFeatureLine(codexConfig)
146
+ const legacyHooksFeatureLine = readLegacyCodexHooksFeatureLine(codexConfig)
147
+
148
+ return {
149
+ checks: {
150
+ carrierMarker: (safeRead(join(codexDir, 'AGENTS.md')) || '').includes('HELLOAGENTS_START'),
151
+ carrierContentMatch: normalizeText((safeRead(join(codexDir, 'AGENTS.md')) || '').match(/<!-- HELLOAGENTS_START -->([\s\S]*?)<!-- HELLOAGENTS_END -->/)?.[1] || '')
152
+ === readExpectedCarrierContent(runtime, expectedHomeCarrier, settings),
153
+ homeLink: homeLinkTarget === (safeRealTarget(runtime.pkgRoot) || normalizePath(runtime.pkgRoot)),
154
+ globalHomeLink: homeLinkTarget === (safeRealTarget(pluginRoot) || normalizePath(pluginRoot)),
155
+ modelInstructionsFile: !!modelInstructionsLine,
156
+ modelInstructionsPathMatch: !!modelInstructionsLine && normalizePath(modelInstructionsLine).includes(`"${CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH}"`),
157
+ codexNotify: codexConfig.includes('codex-notify'),
158
+ notifyPathMatch: codexConfig.includes(CODEX_MANAGED_NOTIFY_VALUE),
159
+ codexHooksFeature: !/^\s*hooks\s*=\s*false\b/.test(hooksFeatureLine)
160
+ && !/^\s*codex_hooks\s*=\s*false\b/.test(legacyHooksFeatureLine),
161
+ codexGoalsFeature: /^\s*goals\s*=\s*true\b/.test(goalsFeatureLine),
162
+ legacyCodexHooksFeature: Boolean(legacyHooksFeatureLine),
163
+ standaloneHooks: JSON.stringify(codexHooks.hooks || {}).includes('helloagents'),
164
+ standaloneHooksMatch: managedHooksMatch(codexHooks.hooks || {}, expectedHooks),
165
+ pluginRoot: existsSync(pluginRoot),
166
+ pluginCache: existsSync(pluginCacheRoot),
167
+ pluginCarrierMatch: normalizeText(safeRead(join(pluginRoot, 'AGENTS.md')) || '') === readExpectedCarrierContent(runtime, 'bootstrap.md', settings),
168
+ pluginCacheCarrierMatch: normalizeText(safeRead(join(pluginCacheRoot, 'AGENTS.md')) || '') === readExpectedCarrierContent(runtime, 'bootstrap.md', settings),
169
+ marketplaceEntry: Array.isArray(marketplace.plugins) && marketplace.plugins.some((plugin) => plugin?.name === CODEX_PLUGIN_NAME),
170
+ pluginEnabled: codexConfig.includes(CODEX_PLUGIN_CONFIG_HEADER) && codexConfig.includes('enabled = true'),
171
+ globalNotifyPathMatch: codexConfig.includes(CODEX_MANAGED_NOTIFY_VALUE),
172
+ pluginVersionMatch: false,
173
+ pluginCacheVersionMatch: false,
174
+ },
175
+ pluginVersion: safeJson(join(pluginRoot, 'package.json'))?.version || '',
176
+ cacheVersion: safeJson(join(pluginCacheRoot, 'package.json'))?.version || '',
177
+ }
178
+ }
179
+
180
+ export function inspectCodexDoctor(runtime, settings) {
181
+ const host = 'codex'
182
+ const trackedMode = normalizeDoctorMode(runtime.getTrackedHostMode(settings, host))
183
+ const detectedMode = normalizeDoctorMode(runtime.detectHostMode(host))
184
+ const { checks, pluginVersion, cacheVersion } = buildCodexChecks(runtime, settings, trackedMode, detectedMode)
185
+ checks.pluginVersionMatch = pluginVersion ? pluginVersion === runtime.pkgVersion : false
186
+ checks.pluginCacheVersionMatch = cacheVersion ? cacheVersion === runtime.pkgVersion : false
187
+
188
+ const issues = []
189
+ const notes = []
190
+ if (trackedMode !== 'none' && detectedMode !== 'none' && trackedMode !== detectedMode) {
191
+ issues.push(buildDoctorIssue(runtime, 'tracked-mode-mismatch', '记录模式与检测模式不一致', 'Tracked mode does not match detected mode'))
192
+ }
193
+ if (detectedMode === 'standby') appendCodexStandbyIssues(runtime, issues, checks)
194
+ if (detectedMode === 'global') appendCodexGlobalIssues(runtime, issues, checks, pluginVersion, cacheVersion)
195
+ if (trackedMode === 'none' && detectedMode !== 'none') {
196
+ issues.push(buildDoctorIssue(runtime, 'untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
197
+ }
198
+ if (trackedMode !== 'none' && detectedMode === 'none') {
199
+ issues.push(buildDoctorIssue(runtime, 'tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应的受管文件或配置', 'Config says this CLI is installed, but no managed artifacts were detected'))
200
+ }
201
+ if (!checks.pluginVersionMatch && !pluginVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件根目录版本信息', 'Global plugin root version was not readable'))
202
+ if (!checks.pluginCacheVersionMatch && !cacheVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件缓存版本信息', 'Global plugin cache version was not readable'))
203
+ if (detectedMode !== 'none' && !checks.codexGoalsFeature) notes.push(runtime.msg('Codex /goal 未启用;如需长程执行,可运行 `helloagents codex goals enable`。', 'Codex /goal is not enabled; run `helloagents codex goals enable` if you need long-running goals.'))
204
+ if (detectedMode !== 'none' && checks.legacyCodexHooksFeature) notes.push(runtime.msg('检测到旧版 `codex_hooks`;HelloAGENTS 只兼容 Codex 最新版,建议移除旧 key。', 'Legacy `codex_hooks` was detected; HelloAGENTS targets latest Codex only, so remove the old key.'))
205
+
206
+ const status = summarizeDoctorStatus(issues, { trackedMode, detectedMode })
207
+ return { host, label: runtime.getHostLabel(host), trackedMode, detectedMode, status, checks, issues, notes, suggestedFix: suggestCodexDoctorFix(status, trackedMode) }
208
+ }
@@ -1,12 +1,8 @@
1
- import { existsSync, realpathSync } from 'node:fs'
1
+ import { realpathSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
- import { CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_CONFIG_HEADER, CODEX_PLUGIN_NAME } from './cli-codex.mjs'
5
- import {
6
- CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
7
- CODEX_MANAGED_NOTIFY_VALUE,
8
- } from './cli-codex-config.mjs'
9
4
  import { DEFAULTS } from './cli-config.mjs'
5
+ import { inspectCodexDoctor as inspectCodexDoctorImpl } from './cli-doctor-codex.mjs'
10
6
  import { printDoctorText } from './cli-doctor-render.mjs'
11
7
  import { buildRuntimeCarrier } from './cli-runtime-carrier.mjs'
12
8
  import { readTopLevelTomlLine } from './cli-toml.mjs'
@@ -223,111 +219,6 @@ function inspectGeminiDoctor(settings) {
223
219
  return { host, label: runtime.getHostLabel(host), trackedMode, detectedMode, status, checks, issues, notes, suggestedFix: suggestDoctorFix(host, status, trackedMode) }
224
220
  }
225
221
 
226
- function appendCodexStandbyIssues(issues, checks) {
227
- if (!checks.carrierMarker) issues.push(buildDoctorIssue('standby-carrier-missing', 'standby 规则文件缺少 HELLOAGENTS 标记', 'Standby carrier is missing the HELLOAGENTS marker'))
228
- if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('standby-carrier-drift', 'standby 规则文件内容与当前 bootstrap-lite.md 不一致', 'Standby carrier content differs from the current bootstrap-lite.md'))
229
- if (!checks.homeLink) issues.push(buildDoctorIssue('standby-link-missing', 'standby home 链接缺失或未指向稳定运行根目录', 'Standby home link is missing or points to a different runtime root'))
230
- if (!checks.modelInstructionsFile) issues.push(buildDoctorIssue('standby-model-instructions-missing', 'standby config 缺少受管 model_instructions_file', 'Standby config is missing the managed model_instructions_file'))
231
- if (checks.modelInstructionsFile && !checks.modelInstructionsPathMatch) issues.push(buildDoctorIssue('standby-model-instructions-drift', 'standby model_instructions_file 未指向受管 `~/.codex/AGENTS.md`', 'Standby model_instructions_file does not point to the managed `~/.codex/AGENTS.md`'))
232
- if (!checks.codexNotify) issues.push(buildDoctorIssue('standby-notify-missing', 'standby notify 配置缺失', 'Standby notify configuration is missing'))
233
- if (checks.codexNotify && !checks.notifyPathMatch) issues.push(buildDoctorIssue('standby-notify-drift', 'standby notify 未使用受管命令入口', 'Standby notify does not use the managed command entrypoint'))
234
- if (checks.pluginRoot || checks.pluginCache || checks.marketplaceEntry || checks.pluginEnabled) {
235
- issues.push(buildDoctorIssue('standby-global-residue', 'standby 模式下仍残留 global 插件文件或配置', 'Global plugin artifacts still remain while Codex is in standby mode'))
236
- }
237
- }
238
-
239
- function appendCodexGlobalIssues(issues, checks, pluginVersion, cacheVersion) {
240
- if (!checks.carrierMarker) issues.push(buildDoctorIssue('global-home-carrier-missing', 'global `~/.codex/AGENTS.md` 缺少 HelloAGENTS 规则内容', 'Global `~/.codex/AGENTS.md` is missing the HelloAGENTS carrier'))
241
- if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue('global-home-carrier-drift', 'global `~/.codex/AGENTS.md` 与当前 bootstrap.md 不一致', 'Global `~/.codex/AGENTS.md` differs from the current bootstrap.md'))
242
- if (!checks.globalHomeLink) issues.push(buildDoctorIssue('global-read-root-link-missing', 'global `~/.codex/helloagents` 链接缺失或未指向当前插件根目录', 'Global `~/.codex/helloagents` link is missing or does not point to the current plugin root'))
243
- if (!checks.pluginRoot) issues.push(buildDoctorIssue('global-plugin-root-missing', 'global 插件根目录缺失', 'Global plugin root is missing'))
244
- if (!checks.pluginCache) issues.push(buildDoctorIssue('global-plugin-cache-missing', 'global 插件缓存目录缺失', 'Global plugin cache directory is missing'))
245
- if (checks.pluginRoot && !checks.pluginCarrierMatch) issues.push(buildDoctorIssue('global-plugin-carrier-drift', 'global 插件根目录中的 AGENTS.md 与当前 bootstrap.md 不一致', 'Global plugin AGENTS.md differs from the current bootstrap.md'))
246
- if (checks.pluginCache && !checks.pluginCacheCarrierMatch) issues.push(buildDoctorIssue('global-plugin-cache-carrier-drift', 'global 插件缓存中的 AGENTS.md 与当前 bootstrap.md 不一致', 'Global plugin cache AGENTS.md differs from the current bootstrap.md'))
247
- if (!checks.marketplaceEntry) issues.push(buildDoctorIssue('global-marketplace-missing', 'global marketplace 条目缺失', 'Global marketplace entry is missing'))
248
- if (!checks.pluginEnabled) issues.push(buildDoctorIssue('global-plugin-disabled', 'global config 中缺少插件启用段', 'Global plugin enablement block is missing from config'))
249
- if (!checks.modelInstructionsFile) issues.push(buildDoctorIssue('global-model-instructions-missing', 'global config 缺少受管 model_instructions_file', 'Global config is missing the managed model_instructions_file'))
250
- if (checks.modelInstructionsFile && !checks.modelInstructionsPathMatch) issues.push(buildDoctorIssue('global-model-instructions-drift', 'global model_instructions_file 未指向受管 `~/.codex/AGENTS.md`', 'Global model_instructions_file does not point to the managed `~/.codex/AGENTS.md`'))
251
- if (!checks.codexNotify) issues.push(buildDoctorIssue('global-notify-missing', 'global notify 配置缺失', 'Global notify configuration is missing'))
252
- if (checks.codexNotify && !checks.globalNotifyPathMatch) issues.push(buildDoctorIssue('global-notify-drift', 'global notify 未使用受管命令入口', 'Global notify does not use the managed command entrypoint'))
253
- if (pluginVersion && !checks.pluginVersionMatch) issues.push(buildDoctorIssue('global-plugin-version-drift', 'global 插件根目录版本与当前包版本不一致', 'Global plugin root version does not match the current package version'))
254
- if (cacheVersion && !checks.pluginCacheVersionMatch) issues.push(buildDoctorIssue('global-plugin-cache-version-drift', 'global 插件缓存版本与当前包版本不一致', 'Global plugin cache version does not match the current package version'))
255
- if (checks.homeLink) {
256
- issues.push(buildDoctorIssue('global-standby-link-residue', 'global 模式下仍残留 standby home 链接', 'Standby home link still remains while Codex is in global mode'))
257
- }
258
- }
259
-
260
- function inspectCodexDoctor(settings) {
261
- const host = 'codex'
262
- const trackedMode = normalizeDoctorMode(runtime.getTrackedHostMode(settings, host))
263
- const detectedMode = normalizeDoctorMode(runtime.detectHostMode(host))
264
- const codexDir = join(runtime.home, '.codex')
265
- const codexConfig = safeRead(join(codexDir, 'config.toml')) || ''
266
- const pluginRoot = join(runtime.home, 'plugins', CODEX_PLUGIN_NAME)
267
- const pluginCacheRoot = join(codexDir, 'plugins', 'cache', CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME, 'local')
268
- const marketplace = safeJson(join(runtime.home, '.agents', 'plugins', 'marketplace.json')) || {}
269
- const pluginVersion = safeJson(join(pluginRoot, 'package.json'))?.version || ''
270
- const cacheVersion = safeJson(join(pluginCacheRoot, 'package.json'))?.version || ''
271
- const homeLinkTarget = safeRealTarget(join(codexDir, 'helloagents'))
272
- const pkgRootTarget = safeRealTarget(runtime.pkgRoot) || normalizePath(runtime.pkgRoot)
273
- const pluginRootTarget = safeRealTarget(pluginRoot) || normalizePath(pluginRoot)
274
- const modelInstructionsLine = readTopLevelTomlLine(codexConfig, 'model_instructions_file')
275
- const expectedHomeCarrier = (detectedMode === 'global' || (detectedMode === 'none' && trackedMode === 'global'))
276
- ? 'bootstrap.md'
277
- : 'bootstrap-lite.md'
278
- const checks = {
279
- carrierMarker: (safeRead(join(codexDir, 'AGENTS.md')) || '').includes('HELLOAGENTS_START'),
280
- carrierContentMatch: extractManagedCarrierContent(join(codexDir, 'AGENTS.md'))
281
- === readExpectedCarrierContent(expectedHomeCarrier, settings),
282
- homeLink: homeLinkTarget === pkgRootTarget,
283
- globalHomeLink: homeLinkTarget === pluginRootTarget,
284
- modelInstructionsFile: !!modelInstructionsLine,
285
- modelInstructionsPathMatch: !!modelInstructionsLine
286
- && normalizePath(modelInstructionsLine).includes(`"${CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH}"`),
287
- codexNotify: codexConfig.includes('codex-notify'),
288
- notifyPathMatch: codexConfig.includes(CODEX_MANAGED_NOTIFY_VALUE),
289
- pluginRoot: existsSync(pluginRoot),
290
- pluginCache: existsSync(pluginCacheRoot),
291
- pluginCarrierMatch: normalizeText(safeRead(join(pluginRoot, 'AGENTS.md')) || '')
292
- === readExpectedCarrierContent('bootstrap.md', settings),
293
- pluginCacheCarrierMatch: normalizeText(safeRead(join(pluginCacheRoot, 'AGENTS.md')) || '')
294
- === readExpectedCarrierContent('bootstrap.md', settings),
295
- marketplaceEntry: Array.isArray(marketplace.plugins) && marketplace.plugins.some((plugin) => plugin?.name === CODEX_PLUGIN_NAME),
296
- pluginEnabled: codexConfig.includes(CODEX_PLUGIN_CONFIG_HEADER) && codexConfig.includes('enabled = true'),
297
- globalNotifyPath: codexConfig.includes('codex-notify'),
298
- globalNotifyPathMatch: codexConfig.includes(CODEX_MANAGED_NOTIFY_VALUE),
299
- pluginVersionMatch: pluginVersion ? pluginVersion === runtime.pkgVersion : false,
300
- pluginCacheVersionMatch: cacheVersion ? cacheVersion === runtime.pkgVersion : false,
301
- }
302
-
303
- const issues = []
304
- const notes = []
305
- if (trackedMode !== 'none' && detectedMode !== 'none' && trackedMode !== detectedMode) {
306
- issues.push(buildDoctorIssue('tracked-mode-mismatch', '记录模式与检测模式不一致', 'Tracked mode does not match detected mode'))
307
- }
308
- if (detectedMode === 'standby') {
309
- appendCodexStandbyIssues(issues, checks)
310
- }
311
- if (detectedMode === 'global') {
312
- appendCodexGlobalIssues(issues, checks, pluginVersion, cacheVersion)
313
- }
314
- if (trackedMode === 'none' && detectedMode !== 'none') {
315
- issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
316
- }
317
- if (trackedMode !== 'none' && detectedMode === 'none') {
318
- issues.push(buildDoctorIssue('tracked-state-missing', '配置记录该 CLI 已安装,但未检测到对应的受管文件或配置', 'Config says this CLI is installed, but no managed artifacts were detected'))
319
- }
320
- if (!checks.pluginVersionMatch && !pluginVersion && detectedMode === 'global') {
321
- notes.push(runtime.msg('未读到 global 插件根目录版本信息', 'Global plugin root version was not readable'))
322
- }
323
- if (!checks.pluginCacheVersionMatch && !cacheVersion && detectedMode === 'global') {
324
- notes.push(runtime.msg('未读到 global 插件缓存版本信息', 'Global plugin cache version was not readable'))
325
- }
326
-
327
- const status = summarizeDoctorStatus(issues, { host, trackedMode, detectedMode })
328
- return { host, label: runtime.getHostLabel(host), trackedMode, detectedMode, status, checks, issues, notes, suggestedFix: suggestDoctorFix(host, status, trackedMode) }
329
- }
330
-
331
222
  function parseDoctorArgs(args) {
332
223
  const wantsJson = args.includes('--json')
333
224
  const unknownFlags = args.filter((arg) => arg.startsWith('--') && arg !== '--json' && arg !== '--all')
@@ -348,7 +239,7 @@ function parseDoctorArgs(args) {
348
239
  function inspectDoctorHost(host, settings) {
349
240
  if (host === 'claude') return inspectClaudeDoctor(settings)
350
241
  if (host === 'gemini') return inspectGeminiDoctor(settings)
351
- return inspectCodexDoctor(settings)
242
+ return inspectCodexDoctorImpl(runtime, settings)
352
243
  }
353
244
 
354
245
  function buildDoctorReport(host) {
@@ -112,6 +112,11 @@ ${msg('诊断', 'Diagnostics')}:
112
112
  helloagents doctor codex --json
113
113
  ${msg('检查 carrier、链接、hooks、配置注入、Codex 插件安装、受管 model_instructions_file 指向与版本漂移', 'Checks carriers, links, hooks, config injections, Codex plugin installation, managed model_instructions_file targeting, and version drift')}
114
114
 
115
+ ${msg('Codex /goal', 'Codex /goal')}:
116
+ helloagents codex goals status
117
+ helloagents codex goals enable
118
+ ${msg('仅显式管理 Codex 最新版 [features].goals,不替代 /goal', 'Explicitly manages only latest Codex [features].goals; does not replace /goal')}
119
+
115
120
  ${msg('卸载', 'Uninstall')}:
116
121
  helloagents cleanup ${msg('(推荐先执行,显式清理所有 CLI 注入/链接)', '(recommended first, explicitly cleans CLI injections/links)')}
117
122
  npm uninstall -g helloagents