helloagents 3.0.39 → 3.1.2

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.
@@ -1,12 +1,11 @@
1
- import { spawnSync } from 'node:child_process'
2
1
  import { existsSync, realpathSync } from 'node:fs'
3
2
  import { platform } from 'node:os'
4
3
  import { join } from 'node:path'
5
4
 
6
5
  import { CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_CONFIG_HEADER, CODEX_PLUGIN_NAME } from './cli-codex.mjs'
7
6
  import {
7
+ analyzeCodexNotifyBlock,
8
8
  CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
9
- CODEX_MANAGED_NOTIFY_VALUE,
10
9
  readCodexGoalsFeatureLine,
11
10
  readCodexHooksFeatureLine,
12
11
  } from './cli-codex-config.mjs'
@@ -16,7 +15,8 @@ import {
16
15
  } from './cli-codex-hooks-state.mjs'
17
16
  import { getStableRuntimeRoot } from './cli-runtime-root.mjs'
18
17
  import { buildRuntimeCarrier } from './cli-runtime-carrier.mjs'
19
- import { readTopLevelTomlLine } from './cli-toml.mjs'
18
+ import { readTopLevelTomlBlock, readTopLevelTomlLine } from './cli-toml.mjs'
19
+ import { spawnCommandSync } from './cli-process.mjs'
20
20
  import { loadHooksWithCliEntry, safeJson, safeRead } from './cli-utils.mjs'
21
21
 
22
22
  function safeRealTarget(linkPath) {
@@ -149,7 +149,7 @@ function summarizeNativeCodexDoctorOutput(payload = {}) {
149
149
  function inspectNativeCodexDoctor(runtime) {
150
150
  const command = platform() === 'win32' ? 'codex.cmd' : 'codex'
151
151
  try {
152
- const result = spawnSync(command, ['doctor', '--json'], {
152
+ const result = spawnCommandSync(command, ['doctor', '--json'], {
153
153
  cwd: process.cwd(),
154
154
  env: {
155
155
  ...process.env,
@@ -159,7 +159,6 @@ function inspectNativeCodexDoctor(runtime) {
159
159
  },
160
160
  encoding: 'utf-8',
161
161
  timeout: 20_000,
162
- shell: platform() === 'win32',
163
162
  windowsHide: true,
164
163
  })
165
164
 
@@ -259,6 +258,8 @@ function appendCodexGlobalIssues(runtime, issues, checks, pluginVersion, cacheVe
259
258
  if (!checks.pluginCache) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-missing', 'global 插件缓存目录缺失', 'Global plugin cache directory is missing'))
260
259
  if (checks.pluginRoot && !checks.pluginRootLink) issues.push(buildDoctorIssue(runtime, 'global-plugin-root-link-drift', 'global 插件根目录未链接到稳定运行根目录', 'Global plugin root does not link to the stable runtime root'))
261
260
  if (checks.pluginCache && !checks.pluginCacheLink) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-link-drift', 'global 插件缓存未链接到稳定运行根目录', 'Global plugin cache does not link to the stable runtime root'))
261
+ if (checks.pluginGenericHooks) issues.push(buildDoctorIssue(runtime, 'global-plugin-generic-hooks-present', 'global 插件根目录中意外存在通用 `hooks/hooks.json`,可能污染 Codex 本地插件 hook 加载', 'Global plugin root unexpectedly contains a generic `hooks/hooks.json`, which can pollute Codex local-plugin hook loading'))
262
+ if (checks.pluginCacheGenericHooks) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-generic-hooks-present', 'global 插件缓存中意外存在通用 `hooks/hooks.json`,可能污染 Codex 本地插件 hook 加载', 'Global plugin cache unexpectedly contains a generic `hooks/hooks.json`, which can pollute Codex local-plugin hook loading'))
262
263
  if (checks.pluginRoot && !checks.pluginCarrierMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-carrier-drift', 'global 插件根目录中的 AGENTS.md 与当前全局模式规则不一致', 'Global plugin AGENTS.md differs from the current global rules'))
263
264
  if (checks.pluginCache && !checks.pluginCacheCarrierMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-carrier-drift', 'global 插件缓存中的 AGENTS.md 与当前全局模式规则不一致', 'Global plugin cache AGENTS.md differs from the current global rules'))
264
265
  if (!checks.marketplaceEntry) issues.push(buildDoctorIssue(runtime, 'global-marketplace-missing', 'global marketplace 条目缺失', 'Global marketplace entry is missing'))
@@ -285,12 +286,16 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
285
286
  const homeLinkTarget = safeRealTarget(join(codexDir, 'helloagents'))
286
287
  const pluginRootTarget = safeRealTarget(pluginRoot)
287
288
  const pluginCacheTarget = safeRealTarget(pluginCacheRoot)
289
+ const pluginGenericHooks = !!safeRead(join(pluginRoot, 'hooks', 'hooks.json'))
290
+ const pluginCacheGenericHooks = !!safeRead(join(pluginCacheRoot, 'hooks', 'hooks.json'))
288
291
  const expectedHomeCarrier = (detectedMode === 'global' || (detectedMode === 'none' && trackedMode === 'global'))
289
292
  ? 'bootstrap.md'
290
293
  : 'bootstrap-lite.md'
291
294
  const codexHooks = safeJson(join(codexDir, 'hooks.json')) || {}
292
295
  const marketplace = safeJson(join(runtime.home, '.agents', 'plugins', 'marketplace.json')) || {}
293
296
  const modelInstructionsLine = readTopLevelTomlLine(codexConfig, 'model_instructions_file')
297
+ const notifyBlock = readTopLevelTomlBlock(codexConfig, 'notify')
298
+ const notifyAnalysis = analyzeCodexNotifyBlock(notifyBlock)
294
299
  const expectedHooks = readExpectedHooks(runtime, 'hooks-codex.json', '${PLUGIN_ROOT}')
295
300
  const expectedHookTrust = buildManagedCodexHookTrustEntries(join(codexDir, 'hooks.json'), codexHooks)
296
301
  const managedHookTrust = new Map(
@@ -314,8 +319,9 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
314
319
  globalHomeLink: homeLinkTarget === runtimeRoot,
315
320
  modelInstructionsFile: !!modelInstructionsLine,
316
321
  modelInstructionsPathMatch: !!modelInstructionsLine && normalizePath(modelInstructionsLine).includes(`"${CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH}"`),
317
- codexNotify: codexConfig.includes('codex-notify'),
318
- notifyPathMatch: codexConfig.includes(CODEX_MANAGED_NOTIFY_VALUE),
322
+ codexNotify: notifyAnalysis.containsCodexNotify,
323
+ notifyPathMatch: notifyAnalysis.managed,
324
+ notifyShape: notifyAnalysis.shape,
319
325
  codexHooksFeature: !/^\s*hooks\s*=\s*false\b/.test(hooksFeatureLine),
320
326
  codexGoalsFeature: /^\s*goals\s*=\s*true\b/.test(goalsFeatureLine),
321
327
  standaloneHooks: JSON.stringify(codexHooks.hooks || {}).includes('helloagents'),
@@ -326,11 +332,13 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
326
332
  pluginCache: existsSync(pluginCacheRoot),
327
333
  pluginRootLink: pluginRootTarget === runtimeRoot,
328
334
  pluginCacheLink: pluginCacheTarget === runtimeRoot,
335
+ pluginGenericHooks,
336
+ pluginCacheGenericHooks,
329
337
  pluginCarrierMatch: normalizeText(safeRead(join(pluginRoot, 'AGENTS.md')) || '') === readExpectedCarrierContent(runtime, 'bootstrap.md', settings, { profile: 'full' }),
330
338
  pluginCacheCarrierMatch: normalizeText(safeRead(join(pluginCacheRoot, 'AGENTS.md')) || '') === readExpectedCarrierContent(runtime, 'bootstrap.md', settings, { profile: 'full' }),
331
339
  marketplaceEntry: Array.isArray(marketplace.plugins) && marketplace.plugins.some((plugin) => plugin?.name === CODEX_PLUGIN_NAME),
332
340
  pluginEnabled: codexConfig.includes(CODEX_PLUGIN_CONFIG_HEADER) && codexConfig.includes('enabled = true'),
333
- globalNotifyPathMatch: codexConfig.includes(CODEX_MANAGED_NOTIFY_VALUE),
341
+ globalNotifyPathMatch: notifyAnalysis.managed,
334
342
  pluginVersionMatch: false,
335
343
  pluginCacheVersionMatch: false,
336
344
  },
@@ -364,6 +372,7 @@ export function inspectCodexDoctor(runtime, settings) {
364
372
  if (!checks.pluginVersionMatch && !pluginVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件根目录版本信息', 'Global plugin root version was not readable'))
365
373
  if (!checks.pluginCacheVersionMatch && !cacheVersion && detectedMode === 'global') notes.push(runtime.msg('未读到 global 插件缓存版本信息', 'Global plugin cache version was not readable'))
366
374
  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.'))
375
+ if (checks.notifyShape === 'chained') notes.push(runtime.msg('HelloAGENTS notify 当前通过 Codex Computer Use / wrapper 链式转发,仍视为有效。', 'HelloAGENTS notify is currently chained through Codex Computer Use / a wrapper and is still treated as valid.'))
367
376
  if (!nativeDoctor.available) notes.push(runtime.msg('未检测到原生 `codex doctor`;当前仅检查 HelloAGENTS 受管覆盖层。', 'Native `codex doctor` was not available; only the HelloAGENTS managed overlay was checked.'))
368
377
 
369
378
  const status = summarizeDoctorStatus(issues, { trackedMode, detectedMode })
@@ -1,12 +1,16 @@
1
- import { realpathSync } from 'node:fs'
1
+ import { existsSync, realpathSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
4
  import { DEFAULTS } from './cli-config.mjs'
5
5
  import { inspectCodexDoctor as inspectCodexDoctorImpl } from './cli-doctor-codex.mjs'
6
6
  import { printDoctorText } from './cli-doctor-render.mjs'
7
7
  import { buildRuntimeCarrier } from './cli-runtime-carrier.mjs'
8
+ import { getClaudeMarketplaceRoot, getGeminiExtensionRoot } from './cli-runtime-root.mjs'
8
9
  import { loadHooksWithCliEntry, safeJson, safeRead } from './cli-utils.mjs'
9
10
 
11
+ const CLAUDE_PLUGIN = 'helloagents@helloagents'
12
+ const GEMINI_EXTENSION = 'helloagents'
13
+
10
14
  const runtime = {
11
15
  home: '',
12
16
  pkgRoot: '',
@@ -93,6 +97,16 @@ function normalizeDoctorMode(mode = '') {
93
97
  return mode || 'none'
94
98
  }
95
99
 
100
+ function hasEnabledPlugin(enabledPlugins, pluginName) {
101
+ if (Array.isArray(enabledPlugins)) {
102
+ return enabledPlugins.includes(pluginName)
103
+ }
104
+ if (enabledPlugins && typeof enabledPlugins === 'object') {
105
+ return Boolean(enabledPlugins[pluginName])
106
+ }
107
+ return false
108
+ }
109
+
96
110
  function summarizeDoctorStatus(issues, { host, trackedMode, detectedMode } = {}) {
97
111
  if (issues.length > 0) return 'drift'
98
112
  if (detectedMode !== 'none') return 'ok'
@@ -106,8 +120,8 @@ function suggestDoctorFix(host, status, trackedMode) {
106
120
  return `helloagents update ${host}${trackedMode && trackedMode !== 'none' ? ` --${trackedMode}` : ''}`
107
121
  }
108
122
  if (status === 'manual-plugin') {
109
- if (host === 'claude') return '/plugin marketplace add https://github.com/hellowind777/helloagents.git; /plugin install helloagents@helloagents'
110
- if (host === 'gemini') return 'helloagents install gemini --global'
123
+ if (host === 'claude') return `/plugin marketplace add "${getClaudeMarketplaceRoot(runtime.home)}"; /plugin install helloagents@helloagents`
124
+ if (host === 'gemini') return `gemini extensions link "${getGeminiExtensionRoot(runtime.home)}"`
111
125
  }
112
126
  if (status === 'not-installed') {
113
127
  return `helloagents install ${host} --standby`
@@ -125,12 +139,18 @@ function inspectClaudeDoctor(settings) {
125
139
  const detectedMode = normalizeDoctorMode(runtime.detectHostMode(host))
126
140
  const claudeDir = join(runtime.home, '.claude')
127
141
  const claudeSettings = safeJson(join(claudeDir, 'settings.json')) || {}
142
+ const claudePlugins = safeJson(join(claudeDir, 'plugins', 'installed_plugins.json')) || {}
128
143
  const expectedHooks = readExpectedHooks('hooks-claude.json', '${CLAUDE_PLUGIN_ROOT}')
144
+ const marketplaceRoot = getClaudeMarketplaceRoot(runtime.home)
145
+ const globalPluginInstalled = Boolean(claudePlugins.plugins?.[CLAUDE_PLUGIN]?.length)
146
+ || hasEnabledPlugin(claudeSettings.enabledPlugins, CLAUDE_PLUGIN)
129
147
  const checks = {
130
148
  carrierMarker: (safeRead(join(claudeDir, 'CLAUDE.md')) || '').includes('HELLOAGENTS_START'),
131
149
  carrierContentMatch: extractManagedCarrierContent(join(claudeDir, 'CLAUDE.md'))
132
150
  === readExpectedCarrierContent('bootstrap-lite.md', settings),
133
151
  homeLink: safeRealTarget(join(claudeDir, 'helloagents')) === runtime.pkgRoot,
152
+ globalMarketplaceRoot: existsSync(marketplaceRoot),
153
+ globalPluginInstalled,
134
154
  settingsHooks: JSON.stringify(claudeSettings.hooks || {}).includes('helloagents'),
135
155
  settingsHooksMatch: managedHooksMatch(claudeSettings.hooks || {}, expectedHooks),
136
156
  settingsPermission: Array.isArray(claudeSettings.permissions?.allow)
@@ -150,14 +170,17 @@ function inspectClaudeDoctor(settings) {
150
170
  if (checks.settingsHooks && !checks.settingsHooksMatch) issues.push(buildDoctorIssue('standby-hooks-drift', 'standby settings hooks 与当前 hooks 配置不一致', 'Standby settings hooks differ from the current hook configuration'))
151
171
  if (!checks.settingsPermission) issues.push(buildDoctorIssue('standby-permission-missing', 'standby Claude 权限注入缺失', 'Standby Claude permission injection is missing'))
152
172
  }
153
- if (trackedMode === 'global') {
154
- notes.push(runtime.msg(
155
- 'Claude Code global 模式由宿主插件系统管理;doctor 只检查 standby 残留,不直接探测插件状态。',
156
- 'Claude Code global mode is managed by the host plugin system; doctor only checks for standby residue and does not inspect plugin state directly.',
157
- ))
173
+ if (detectedMode === 'global') {
174
+ if (!checks.globalMarketplaceRoot) issues.push(buildDoctorIssue('global-marketplace-root-missing', 'global marketplace 投影缺失', 'Global marketplace projection is missing'))
175
+ if (!checks.globalPluginInstalled) issues.push(buildDoctorIssue('global-plugin-missing', 'global Claude 插件未安装', 'Global Claude plugin is not installed'))
158
176
  if (checks.carrierMarker || checks.homeLink || checks.settingsHooks || checks.settingsPermission) {
159
- issues.push(buildDoctorIssue('global-standby-residue', 'global 模式下仍残留 standby 注入/链接', 'Standby injections or links still remain while the host is tracked as global'))
177
+ issues.push(buildDoctorIssue('global-standby-residue', 'global 模式下仍残留 standby 注入/链接', 'Standby injections or links still remain while the host is detected as global'))
160
178
  }
179
+ } else if (trackedMode === 'global') {
180
+ notes.push(runtime.msg(
181
+ 'Claude Code 的 global 模式由宿主插件系统管理;doctor 会检查本地 marketplace 投影、已安装插件记录与 standby 残留。',
182
+ 'Claude Code global mode is managed by the host plugin system; doctor checks the local marketplace projection, installed-plugin records, and standby residue.',
183
+ ))
161
184
  }
162
185
  if (trackedMode === 'none' && detectedMode !== 'none') {
163
186
  issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
@@ -177,11 +200,17 @@ function inspectGeminiDoctor(settings) {
177
200
  const geminiDir = join(runtime.home, '.gemini')
178
201
  const geminiSettings = safeJson(join(geminiDir, 'settings.json')) || {}
179
202
  const expectedHooks = readExpectedHooks('hooks-gemini.json', '${extensionPath}')
203
+ const extensionRoot = getGeminiExtensionRoot(runtime.home)
204
+ const extensionInstallRoot = join(geminiDir, 'extensions', GEMINI_EXTENSION)
205
+ const expectedExtensionTarget = safeRealTarget(extensionRoot) || normalizePath(extensionRoot)
180
206
  const checks = {
181
207
  carrierMarker: (safeRead(join(geminiDir, 'GEMINI.md')) || '').includes('HELLOAGENTS_START'),
182
208
  carrierContentMatch: extractManagedCarrierContent(join(geminiDir, 'GEMINI.md'))
183
209
  === readExpectedCarrierContent('bootstrap-lite.md', settings),
184
210
  homeLink: safeRealTarget(join(geminiDir, 'helloagents')) === runtime.pkgRoot,
211
+ globalExtensionRoot: existsSync(extensionRoot),
212
+ globalExtensionLink: safeRealTarget(extensionInstallRoot) === expectedExtensionTarget,
213
+ globalExtensionInstall: existsSync(extensionInstallRoot),
185
214
  settingsHooks: JSON.stringify(geminiSettings.hooks || {}).includes('helloagents'),
186
215
  settingsHooksMatch: managedHooksMatch(geminiSettings.hooks || {}, expectedHooks),
187
216
  }
@@ -198,14 +227,18 @@ function inspectGeminiDoctor(settings) {
198
227
  if (!checks.settingsHooks) issues.push(buildDoctorIssue('standby-hooks-missing', 'standby settings hooks 缺失', 'Standby settings hooks are missing'))
199
228
  if (checks.settingsHooks && !checks.settingsHooksMatch) issues.push(buildDoctorIssue('standby-hooks-drift', 'standby settings hooks 与当前 hooks 配置不一致', 'Standby settings hooks differ from the current hook configuration'))
200
229
  }
201
- if (trackedMode === 'global') {
202
- notes.push(runtime.msg(
203
- 'Gemini CLI global 模式由宿主扩展系统管理;doctor 只检查 standby 残留,不直接探测扩展状态。',
204
- 'Gemini CLI global mode is managed by the host extension system; doctor only checks for standby residue and does not inspect extension state directly.',
205
- ))
230
+ if (detectedMode === 'global') {
231
+ if (!checks.globalExtensionRoot) issues.push(buildDoctorIssue('global-extension-root-missing', 'global extension 投影缺失', 'Global extension projection is missing'))
232
+ if (!checks.globalExtensionInstall) issues.push(buildDoctorIssue('global-extension-missing', 'global Gemini 扩展未安装', 'Global Gemini extension is not installed'))
233
+ if (!checks.globalExtensionLink) issues.push(buildDoctorIssue('global-extension-link-missing', 'global Gemini 扩展链接未指向投影目录', 'Global Gemini extension link does not point to the projection root'))
206
234
  if (checks.carrierMarker || checks.homeLink || checks.settingsHooks) {
207
- issues.push(buildDoctorIssue('global-standby-residue', 'global 模式下仍残留 standby 注入/链接', 'Standby injections or links still remain while the host is tracked as global'))
235
+ issues.push(buildDoctorIssue('global-standby-residue', 'global 模式下仍残留 standby 注入/链接', 'Standby injections or links still remain while the host is detected as global'))
208
236
  }
237
+ } else if (trackedMode === 'global') {
238
+ notes.push(runtime.msg(
239
+ 'Gemini CLI 的 global 模式由宿主扩展系统管理;doctor 会检查本地扩展投影、已安装链接与 standby 残留。',
240
+ 'Gemini CLI global mode is managed by the host extension system; doctor checks the local extension projection, installed link, and standby residue.',
241
+ ))
209
242
  }
210
243
  if (trackedMode === 'none' && detectedMode !== 'none') {
211
244
  issues.push(buildDoctorIssue('untracked-managed-state', '检测到受管状态,但配置中未记录该 CLI 模式', 'Managed state detected but this CLI mode is not tracked in config'))
@@ -6,9 +6,12 @@ import {
6
6
  CODEX_PLUGIN_KEY,
7
7
  CODEX_PLUGIN_NAME,
8
8
  } from './cli-codex.mjs'
9
- import { getStableRuntimeRoot } from './cli-runtime-root.mjs'
9
+ import { getGeminiExtensionRoot, getStableRuntimeRoot } from './cli-runtime-root.mjs'
10
10
  import { safeJson, safeRead } from './cli-utils.mjs'
11
11
 
12
+ const CLAUDE_PLUGIN = 'helloagents@helloagents'
13
+ const GEMINI_EXTENSION = 'helloagents'
14
+
12
15
  const HOST_ALIASES = new Map([
13
16
  ['all', 'all'],
14
17
  ['*', 'all'],
@@ -24,8 +27,14 @@ function hasHelloagentsMarker(filePath) {
24
27
  return (safeRead(filePath) || '').includes('HELLOAGENTS_START')
25
28
  }
26
29
 
27
- function hasHelloagentsSettings(filePath) {
28
- return JSON.stringify(safeJson(filePath) || {}).includes('helloagents')
30
+ function hasHelloagentsSettings(filePath, host = '') {
31
+ const settings = safeJson(filePath) || {}
32
+ const hooksText = JSON.stringify(settings.hooks || {})
33
+ if (hooksText.includes('helloagents')) return true
34
+ if (host === 'claude') {
35
+ return JSON.stringify(settings.permissions?.allow || []).includes('~/.helloagents/helloagents')
36
+ }
37
+ return false
29
38
  }
30
39
 
31
40
  function normalizePath(value = '') {
@@ -40,12 +49,27 @@ function safeRealTarget(linkPath) {
40
49
  }
41
50
  }
42
51
 
52
+ function hasEnabledPlugin(enabledPlugins, pluginName) {
53
+ if (Array.isArray(enabledPlugins)) {
54
+ return enabledPlugins.includes(pluginName)
55
+ }
56
+ if (enabledPlugins && typeof enabledPlugins === 'object') {
57
+ return Boolean(enabledPlugins[pluginName])
58
+ }
59
+ return false
60
+ }
61
+
43
62
  function detectClaudeMode(home) {
44
63
  const claudeDir = join(home, '.claude')
64
+ const settings = safeJson(join(claudeDir, 'settings.json')) || {}
65
+ const installedPlugins = safeJson(join(claudeDir, 'plugins', 'installed_plugins.json')) || {}
66
+ if (hasEnabledPlugin(settings.enabledPlugins, CLAUDE_PLUGIN) || installedPlugins.plugins?.[CLAUDE_PLUGIN]?.length) {
67
+ return 'global'
68
+ }
45
69
  if (
46
70
  existsSync(join(claudeDir, 'helloagents'))
47
71
  || hasHelloagentsMarker(join(claudeDir, 'CLAUDE.md'))
48
- || hasHelloagentsSettings(join(claudeDir, 'settings.json'))
72
+ || hasHelloagentsSettings(join(claudeDir, 'settings.json'), 'claude')
49
73
  ) {
50
74
  return 'standby'
51
75
  }
@@ -54,10 +78,19 @@ function detectClaudeMode(home) {
54
78
 
55
79
  function detectGeminiMode(home) {
56
80
  const geminiDir = join(home, '.gemini')
81
+ const extensionRoot = safeRealTarget(getGeminiExtensionRoot(home)) || normalizePath(getGeminiExtensionRoot(home))
82
+ const installedExtensionRoot = join(geminiDir, 'extensions', GEMINI_EXTENSION)
83
+ const installedExtension = safeJson(join(installedExtensionRoot, 'gemini-extension.json')) || {}
84
+ if (
85
+ existsSync(installedExtensionRoot)
86
+ && (safeRealTarget(installedExtensionRoot) === extensionRoot || installedExtension.name === GEMINI_EXTENSION)
87
+ ) {
88
+ return 'global'
89
+ }
57
90
  if (
58
91
  existsSync(join(geminiDir, 'helloagents'))
59
92
  || hasHelloagentsMarker(join(geminiDir, 'GEMINI.md'))
60
- || hasHelloagentsSettings(join(geminiDir, 'settings.json'))
93
+ || hasHelloagentsSettings(join(geminiDir, 'settings.json'), 'gemini')
61
94
  ) {
62
95
  return 'standby'
63
96
  }
@@ -1,4 +1,3 @@
1
- import { spawnSync } from 'node:child_process'
2
1
  import { platform } from 'node:os'
3
2
 
4
3
  import {
@@ -14,11 +13,22 @@ import {
14
13
  uninstallCodexGlobal,
15
14
  uninstallCodexStandby,
16
15
  } from './cli-codex.mjs'
17
- import { getHostLabel } from './cli-host-detect.mjs'
16
+ import { spawnCommandSync } from './cli-process.mjs'
17
+ import {
18
+ detectHostMode as detectRuntimeHostMode,
19
+ getHostLabel,
20
+ } from './cli-host-detect.mjs'
21
+ import {
22
+ getClaudeMarketplaceRoot,
23
+ getGeminiExtensionRoot,
24
+ removeClaudeMarketplaceRoot,
25
+ removeGeminiExtensionRoot,
26
+ syncClaudeMarketplaceRoot,
27
+ syncGeminiExtensionRoot,
28
+ } from './cli-runtime-root.mjs'
18
29
 
19
30
  const CLAUDE_COMMAND = process.env.HELLOAGENTS_CLAUDE_CMD || 'claude'
20
31
  const GEMINI_COMMAND = process.env.HELLOAGENTS_GEMINI_CMD || 'gemini'
21
- const CLAUDE_MARKETPLACE = 'https://github.com/hellowind777/helloagents.git'
22
32
  const CLAUDE_PLUGIN = 'helloagents@helloagents'
23
33
 
24
34
  function normalizeCommand(command = '') {
@@ -44,11 +54,9 @@ function runHostCommand(command, args) {
44
54
  let lastResult = null
45
55
 
46
56
  for (const candidate of attempts) {
47
- const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(candidate)
48
- const result = spawnSync(candidate, args, {
57
+ const result = spawnCommandSync(candidate, args, {
49
58
  encoding: 'utf-8',
50
59
  errors: 'replace',
51
- shell: needsShell,
52
60
  windowsHide: true,
53
61
  })
54
62
  lastResult = result
@@ -81,8 +89,8 @@ function preserveTrackedModeOnFailure(result = {}, trackedMode = '') {
81
89
  return result
82
90
  }
83
91
 
84
- function installClaudeGlobalPlugin() {
85
- const add = runHostCommand(CLAUDE_COMMAND, ['plugin', 'marketplace', 'add', CLAUDE_MARKETPLACE])
92
+ function installClaudeGlobalPlugin(marketplaceRoot) {
93
+ const add = runHostCommand(CLAUDE_COMMAND, ['plugin', 'marketplace', 'add', marketplaceRoot])
86
94
  if (!add.ok && add.missing) return { ok: false, output: '未找到 claude 命令' }
87
95
  const install = runHostCommand(CLAUDE_COMMAND, ['plugin', 'install', CLAUDE_PLUGIN, '--scope', 'user'])
88
96
  return { ok: install.ok, output: install.output || add.output }
@@ -158,12 +166,14 @@ function installHostStandby(runtime, host, { previousMode = '' } = {}) {
158
166
  const cleanupResult = prepareClaudeStandby(previousMode)
159
167
  if (cleanupResult.ok === false) return cleanupResult
160
168
  installClaudeStandby(runtime.home, runtime.pkgRoot)
169
+ if (detectRuntimeHostMode('claude', runtime) !== 'global') removeClaudeMarketplaceRoot(runtime.home)
161
170
  return cleanupResult
162
171
  }
163
172
  if (host === 'gemini') {
164
173
  const cleanupResult = prepareGeminiStandby(previousMode)
165
174
  if (cleanupResult.ok === false) return cleanupResult
166
175
  installGeminiStandby(runtime.home, runtime.pkgRoot)
176
+ if (detectRuntimeHostMode('gemini', runtime) !== 'global') removeGeminiExtensionRoot(runtime.home)
167
177
  return cleanupResult
168
178
  }
169
179
  if (!installCodexStandby(runtime.home, runtime.pkgRoot)) return { skipped: true }
@@ -174,31 +184,45 @@ function installHostStandby(runtime, host, { previousMode = '' } = {}) {
174
184
  function installHostGlobal(runtime, host) {
175
185
  if (host === 'claude') {
176
186
  uninstallClaudeStandby(runtime.home)
177
- return buildNativeResult(
178
- installClaudeGlobalPlugin(),
187
+ const marketplaceRoot = getClaudeMarketplaceRoot(runtime.home)
188
+ syncClaudeMarketplaceRoot(runtime.pkgRoot, marketplaceRoot)
189
+ const result = buildNativeResult(
190
+ installClaudeGlobalPlugin(marketplaceRoot),
179
191
  '已自动安装 Claude Code 插件;重启 Claude Code 后生效',
180
192
  'Claude Code plugin installed automatically; restart Claude Code to apply',
181
- 'Claude Code 插件自动安装失败,请在 Claude Code 中执行: /plugin marketplace add https://github.com/hellowind777/helloagents.git;/plugin install helloagents@helloagents',
182
- 'Claude Code plugin auto-install failed. Run inside Claude Code: /plugin marketplace add https://github.com/hellowind777/helloagents.git; /plugin install helloagents@helloagents',
193
+ `Claude Code 插件自动安装失败,请在 Claude Code 中执行: /plugin marketplace add "${marketplaceRoot}";/plugin install helloagents@helloagents`,
194
+ `Claude Code plugin auto-install failed. Run inside Claude Code: /plugin marketplace add "${marketplaceRoot}"; /plugin install helloagents@helloagents`,
183
195
  )
196
+ return result
184
197
  }
185
198
  if (host === 'gemini') {
186
199
  uninstallGeminiStandby(runtime.home)
187
- return buildNativeResult(
188
- installGeminiGlobalExtension(runtime.pkgRoot),
200
+ const extensionRoot = getGeminiExtensionRoot(runtime.home)
201
+ syncGeminiExtensionRoot(runtime.pkgRoot, extensionRoot)
202
+ const result = buildNativeResult(
203
+ installGeminiGlobalExtension(extensionRoot),
189
204
  '已自动安装 Gemini CLI 扩展;重启 Gemini CLI 后生效',
190
205
  'Gemini CLI extension installed automatically; restart Gemini CLI to apply',
191
- `Gemini CLI 扩展自动安装失败,请手动执行: gemini extensions link ${runtime.pkgRoot}`,
192
- `Gemini CLI extension auto-install failed. Run manually: gemini extensions link ${runtime.pkgRoot}`,
206
+ `Gemini CLI 扩展自动安装失败,请手动执行: gemini extensions link "${extensionRoot}"`,
207
+ `Gemini CLI extension auto-install failed. Run manually: gemini extensions link "${extensionRoot}"`,
193
208
  )
209
+ return result
194
210
  }
195
211
  uninstallCodexStandby(runtime.home)
196
212
  return installCodexGlobal(runtime.home, runtime.pkgRoot) ? {} : { skipped: true }
197
213
  }
198
214
 
199
215
  function cleanupHostStandby(runtime, host) {
200
- if (host === 'claude') return { skipped: !uninstallClaudeStandby(runtime.home) }
201
- if (host === 'gemini') return { skipped: !uninstallGeminiStandby(runtime.home) }
216
+ if (host === 'claude') {
217
+ const skipped = !uninstallClaudeStandby(runtime.home)
218
+ if (detectRuntimeHostMode('claude', runtime) !== 'global') removeClaudeMarketplaceRoot(runtime.home)
219
+ return { skipped }
220
+ }
221
+ if (host === 'gemini') {
222
+ const skipped = !uninstallGeminiStandby(runtime.home)
223
+ if (detectRuntimeHostMode('gemini', runtime) !== 'global') removeGeminiExtensionRoot(runtime.home)
224
+ return { skipped }
225
+ }
202
226
  const standbyCleaned = uninstallCodexStandby(runtime.home)
203
227
  const globalResidueCleaned = uninstallCodexGlobal(runtime.home)
204
228
  return { skipped: !(standbyCleaned || globalResidueCleaned) }
@@ -207,7 +231,7 @@ function cleanupHostStandby(runtime, host) {
207
231
  function cleanupHostGlobal(runtime, host) {
208
232
  if (host === 'claude') {
209
233
  uninstallClaudeStandby(runtime.home)
210
- return preserveTrackedModeOnFailure(
234
+ const result = preserveTrackedModeOnFailure(
211
235
  buildNativeResult(
212
236
  removeClaudeGlobalPlugin(),
213
237
  '已自动移除 Claude Code 插件',
@@ -217,10 +241,12 @@ function cleanupHostGlobal(runtime, host) {
217
241
  ),
218
242
  'global',
219
243
  )
244
+ if (result.ok) removeClaudeMarketplaceRoot(runtime.home)
245
+ return result
220
246
  }
221
247
  if (host === 'gemini') {
222
248
  uninstallGeminiStandby(runtime.home)
223
- return preserveTrackedModeOnFailure(
249
+ const result = preserveTrackedModeOnFailure(
224
250
  buildNativeResult(
225
251
  removeGeminiGlobalExtension(),
226
252
  '已自动移除 Gemini CLI 扩展',
@@ -230,6 +256,8 @@ function cleanupHostGlobal(runtime, host) {
230
256
  ),
231
257
  'global',
232
258
  )
259
+ if (result.ok) removeGeminiExtensionRoot(runtime.home)
260
+ return result
233
261
  }
234
262
  return { skipped: !uninstallCodexGlobal(runtime.home) }
235
263
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
+ import { getClaudeMarketplaceRoot, getGeminiExtensionRoot } from './cli-runtime-root.mjs'
3
4
 
4
5
  export function createMessageHelpers(isCN) {
5
6
  const msg = (cn, en) => (isCN ? cn : en)
@@ -19,11 +20,11 @@ function codexGlobalStatus({ home, msg }) {
19
20
  : msg('安装 Codex CLI 后重新运行 npm install -g helloagents', 'Install Codex CLI then re-run npm install -g helloagents')
20
21
  }
21
22
 
22
- function pluginCommands() {
23
+ function pluginCommands(home) {
23
24
  return [
24
- ' Claude Code: /plugin marketplace add https://github.com/hellowind777/helloagents.git',
25
+ ` Claude Code: /plugin marketplace add "${getClaudeMarketplaceRoot(home)}"`,
25
26
  ' /plugin install helloagents@helloagents',
26
- ' Gemini CLI: helloagents install gemini --global',
27
+ ` Gemini CLI: gemini extensions link "${getGeminiExtensionRoot(home)}"`,
27
28
  ].join('\n')
28
29
  }
29
30
 
@@ -42,24 +43,24 @@ function restartHint(msg) {
42
43
  }
43
44
 
44
45
  function renderInstallMessage(context, mode, state) {
45
- const { msg } = context
46
+ const { home, msg } = context
46
47
  const install = state === 'install'
47
48
  const refresh = state === 'refresh'
48
49
 
49
50
  if (mode === 'global') {
50
51
  if (install) {
51
52
  return msg(
52
- `\n ✅ HelloAGENTS 已安装(global 模式)!\n\n Claude Code / Gemini CLI: 已自动尝试宿主原生插件/扩展安装\n Codex: ${codexGlobalStatus(context)}(~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n 若宿主命令不可用,请手动执行:\n${pluginCommands()}\n\n 切换模式:\n helloagents --standby 标准模式(默认,非插件安装)`,
53
- `\n ✅ HelloAGENTS installed (global mode)!\n\n Claude Code / Gemini CLI: native plugin/extension install attempted automatically\n Codex: ${codexGlobalStatus(context)} (~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n If a host command is unavailable, run manually:\n${pluginCommands()}\n\n Switch modes:\n helloagents --standby Standby mode (default, non-plugin install)`,
53
+ `\n ✅ HelloAGENTS 已安装(global 模式)!\n\n Claude Code / Gemini CLI: 已自动尝试宿主原生插件/扩展安装\n Codex: ${codexGlobalStatus(context)}(~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n 若宿主命令不可用,请手动执行:\n${pluginCommands(home)}\n\n 切换模式:\n helloagents --standby 标准模式(默认,非插件安装)`,
54
+ `\n ✅ HelloAGENTS installed (global mode)!\n\n Claude Code / Gemini CLI: native plugin/extension install attempted automatically\n Codex: ${codexGlobalStatus(context)} (~/.agents/plugins/marketplace.json + ~/plugins/helloagents)\n\n ${restartHint(msg)}\n\n If a host command is unavailable, run manually:\n${pluginCommands(home)}\n\n Switch modes:\n helloagents --standby Standby mode (default, non-plugin install)`,
54
55
  )
55
56
  }
56
57
  return msg(
57
58
  refresh
58
59
  ? ` global 模式已刷新。\n Claude Code / Gemini 已自动尝试刷新宿主插件/扩展;Codex 原生本地插件已重装并同步最新文件。\n ${restartHint(msg)}`
59
- : ` 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 已自动尝试安装宿主插件/扩展;Codex 已自动安装原生本地插件。\n ${restartHint(msg)}\n\n若宿主命令不可用,请手动执行:\n${pluginCommands()}`,
60
+ : ` 所有项目将自动启用完整 HelloAGENTS 规则。\n Claude Code / Gemini 已自动尝试安装宿主插件/扩展;Codex 已自动安装原生本地插件。\n ${restartHint(msg)}\n\n若宿主命令不可用,请手动执行:\n${pluginCommands(home)}`,
60
61
  refresh
61
62
  ? ` Global mode refreshed.\n Claude Code / Gemini native plugin/extension refresh was attempted automatically; Codex native local-plugin files were reinstalled and synced.\n ${restartHint(msg)}`
62
- : ` All projects will use full HelloAGENTS rules.\n Claude Code / Gemini native plugin/extension install was attempted automatically; Codex now uses the native local-plugin path automatically.\n ${restartHint(msg)}\n\nIf a host command is unavailable, run manually:\n${pluginCommands()}`,
63
+ : ` All projects will use full HelloAGENTS rules.\n Claude Code / Gemini native plugin/extension install was attempted automatically; Codex now uses the native local-plugin path automatically.\n ${restartHint(msg)}\n\nIf a host command is unavailable, run manually:\n${pluginCommands(home)}`,
63
64
  )
64
65
  }
65
66
 
@@ -0,0 +1,16 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ /**
4
+ * Run a command on all platforms, including Windows .cmd/.bat files, without
5
+ * relying on child_process shell=true argument concatenation.
6
+ */
7
+ export function spawnCommandSync(command, args = [], options = {}) {
8
+ const normalizedArgs = Array.isArray(args) ? args.map((arg) => String(arg)) : []
9
+ const isWindowsShellScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(String(command || ''))
10
+ if (!isWindowsShellScript) {
11
+ return spawnSync(command, normalizedArgs, options)
12
+ }
13
+
14
+ const comspec = process.env.ComSpec || 'cmd.exe'
15
+ return spawnSync(comspec, ['/d', '/s', '/c', String(command || ''), ...normalizedArgs], options)
16
+ }
@@ -1,7 +1,7 @@
1
1
  import { copyFileSync, existsSync, mkdtempSync, realpathSync, renameSync } from 'node:fs'
2
2
  import { dirname, join, resolve } from 'node:path'
3
3
 
4
- import { copyEntries, ensureDir, removeIfExists } from './cli-utils.mjs'
4
+ import { copyEntries, createLink, ensureDir, removeIfExists } from './cli-utils.mjs'
5
5
 
6
6
  export const RUNTIME_ROOT_ENTRIES = [
7
7
  '.claude-plugin',
@@ -28,6 +28,16 @@ export function getStableRuntimeRoot(home) {
28
28
  return join(home, '.helloagents', 'helloagents')
29
29
  }
30
30
 
31
+ /** Return the Claude local marketplace projection root derived from the shared runtime copy. */
32
+ export function getClaudeMarketplaceRoot(home) {
33
+ return join(home, '.helloagents', 'host-projections', 'claude-marketplace')
34
+ }
35
+
36
+ /** Return the Gemini extension projection root derived from the shared runtime copy. */
37
+ export function getGeminiExtensionRoot(home) {
38
+ return join(home, '.helloagents', 'host-projections', 'gemini')
39
+ }
40
+
31
41
  function normalizePath(path) {
32
42
  const resolved = resolve(path)
33
43
  try {
@@ -63,17 +73,9 @@ function retryTransientFs(operation) {
63
73
  throw lastError
64
74
  }
65
75
 
66
- function materializeGeminiHooks(root) {
67
- const source = join(root, 'hooks', 'hooks-gemini.json')
68
- const target = join(root, 'hooks', 'hooks.json')
69
- if (!existsSync(source)) return
70
- copyFileSync(source, target)
71
- }
72
-
73
- /** Sync package runtime files into the stable root without copying repo-only files. */
74
- export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
76
+ function syncRuntimeTree(sourceRoot, targetRoot, { materializeGeminiHooks = false } = {}) {
75
77
  const source = resolve(sourceRoot)
76
- const target = resolve(runtimeRoot)
78
+ const target = resolve(targetRoot)
77
79
  if (samePath(source, target)) {
78
80
  return { synced: false, root: target }
79
81
  }
@@ -84,7 +86,13 @@ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
84
86
 
85
87
  try {
86
88
  copyEntries(source, staging, RUNTIME_ROOT_ENTRIES)
87
- materializeGeminiHooks(staging)
89
+ if (materializeGeminiHooks) {
90
+ const sourceHooks = join(staging, 'hooks', 'hooks-gemini.json')
91
+ const targetHooks = join(staging, 'hooks', 'hooks.json')
92
+ if (existsSync(sourceHooks)) {
93
+ copyFileSync(sourceHooks, targetHooks)
94
+ }
95
+ }
88
96
  retryTransientFs(() => {
89
97
  removeIfExists(target)
90
98
  renameSync(staging, target)
@@ -96,7 +104,42 @@ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
96
104
  }
97
105
  }
98
106
 
107
+ /** Sync package runtime files into the stable root without copying repo-only files. */
108
+ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
109
+ return syncRuntimeTree(sourceRoot, runtimeRoot)
110
+ }
111
+
112
+ /** Sync a Claude local marketplace root that resolves to the stable runtime copy. */
113
+ export function syncClaudeMarketplaceRoot(sourceRoot, marketplaceRoot) {
114
+ const source = resolve(sourceRoot)
115
+ const target = resolve(marketplaceRoot)
116
+ if (samePath(source, target)) {
117
+ return { synced: false, root: target }
118
+ }
119
+
120
+ removeIfExists(target)
121
+ if (createLink(source, target)) {
122
+ return { synced: true, root: target }
123
+ }
124
+ return syncRuntimeTree(source, target)
125
+ }
126
+
127
+ /** Sync a host-specific extension root derived from the stable runtime copy. */
128
+ export function syncGeminiExtensionRoot(sourceRoot, extensionRoot) {
129
+ return syncRuntimeTree(sourceRoot, extensionRoot, { materializeGeminiHooks: true })
130
+ }
131
+
99
132
  /** Remove the stable runtime copy while leaving user settings under ~/.helloagents intact. */
100
133
  export function removeRuntimeRoot(runtimeRoot) {
101
134
  removeIfExists(runtimeRoot)
102
135
  }
136
+
137
+ /** Remove the Claude marketplace projection root. */
138
+ export function removeClaudeMarketplaceRoot(home) {
139
+ removeIfExists(getClaudeMarketplaceRoot(home))
140
+ }
141
+
142
+ /** Remove the Gemini extension projection root. */
143
+ export function removeGeminiExtensionRoot(home) {
144
+ removeIfExists(getGeminiExtensionRoot(home))
145
+ }