helloagents 3.0.39 → 3.1.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.39",
3
+ "version": "3.1.1",
4
4
  "description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, unified QA gates, safety guards, and notifications.",
5
5
  "author": {
6
6
  "name": "HelloWind",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.39",
3
+ "version": "3.1.1",
4
4
  "description": "HelloAGENTS — Quality-driven orchestration kernel for AI CLIs with intelligent routing, unified QA gates, safety guards, and notifications.",
5
5
  "author": {
6
6
  "name": "HelloWind",
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **A workflow layer for AI coding CLIs: skills, project knowledge, delivery checks, safer config writes, and resumable execution.**
10
10
 
11
- [![Version](https://img.shields.io/badge/version-3.0.39-orange.svg)](./package.json)
11
+ [![Version](https://img.shields.io/badge/version-3.1.1-orange.svg)](./package.json)
12
12
  [![npm](https://img.shields.io/npm/v/helloagents.svg)](https://www.npmjs.com/package/helloagents)
13
13
  [![Node](https://img.shields.io/badge/node-%3E%3D18-339933.svg)](./package.json)
14
14
  [![Skills](https://img.shields.io/badge/skills-14-6366f1.svg)](./skills)
@@ -225,8 +225,10 @@ The CLI manages host files explicitly:
225
225
  - `update` refreshes the selected target or all targets
226
226
  - `cleanup` removes managed injections and links
227
227
  - `uninstall` performs scoped cleanup before package removal
228
- - `doctor` reports drift in carriers, links, hooks, config entries, plugin roots, cache copies, and versions; for Codex, it also surfaces native `codex doctor` output when available
228
+ - `doctor` reports drift in carriers, links, hooks, config entries, plugin roots, cache copies, versions, and real Claude/Gemini global install artifacts; for Codex, it also surfaces native `codex doctor` output when available
229
+ - Codex managed `notify = ["helloagents-js", "codex-notify"]` stays portable, and `doctor`, `cleanup`, and `uninstall` also recognize wrapped `--previous-notify` chains used by Codex App / Computer Use
229
230
  - per-host mode tracking is written only after host setup succeeds, and failed native global cleanup keeps the host tracked as `global` instead of silently layering standby on top
231
+ - Windows `.cmd` / `.bat` lifecycle calls now run through an explicit command wrapper, so host installs, branch switching, and doctor flows do not emit Node `DEP0190` shell deprecation warnings
230
232
 
231
233
  ## Quick Start
232
234
 
@@ -313,7 +315,7 @@ If you omit `--standby` or `--global`, HelloAGENTS first reuses the tracked/dete
313
315
 
314
316
  Use these when you do not want to depend on the `helloagents` binary being available during package updates. In `HELLOAGENTS=target[:mode]`, target can be `all`, `claude`, `gemini`, or `codex`; mode can be `standby` or `global`. For install, an omitted mode is treated as `standby`. For update, cleanup, uninstall, and branch switching, an omitted mode is forwarded unchanged so HelloAGENTS can reuse the tracked or detected mode for that CLI first. If you do not provide `HELLOAGENTS`, the one-shot install scripts now behave like plain package install: they install or update the package only and do not auto-deploy any host CLI. For a custom tarball or package spec, set `HELLOAGENTS_PACKAGE` instead of `HELLOAGENTS_BRANCH`. For a guaranteed refresh of an already installed package, prefer `npm explore -g helloagents -- npm run sync-hosts -- ...` after the package command.
315
317
 
316
- Host configs use the stable `helloagents-js` entrypoint and runtime root `~/.helloagents/helloagents`, so Node global package paths can change without breaking managed hooks or Codex `notify`. Codex hooks use standalone `~/.codex/hooks.json` instead of adding large hook blocks to `config.toml`, and Codex global plugin roots plus plugin cache now link back to that same stable runtime root.
318
+ Host configs use the stable `helloagents-js` entrypoint and runtime root `~/.helloagents/helloagents`, so Node global package paths can change without breaking managed hooks or Codex `notify`. Codex hooks use standalone `~/.codex/hooks.json` instead of adding large hook blocks to `config.toml`, and Codex global plugin roots plus plugin cache now link back to that same stable runtime root. Claude Code global installs now use a dedicated local marketplace projection under `~/.helloagents/host-projections/claude-marketplace`, and Gemini global extension packaging uses `~/.helloagents/host-projections/gemini`, so host-specific packaging stays isolated from the shared runtime root.
317
319
 
318
320
  #### npm commands
319
321
 
@@ -445,16 +447,16 @@ npm uninstall -g helloagents
445
447
 
446
448
  | CLI | Install method | Files involved |
447
449
  |-----|----------------|----------------|
448
- | Claude Code | native plugin install | managed by Claude Code plugin system |
449
- | Gemini CLI | native extension install | managed by Gemini extension system |
450
+ | Claude Code | native plugin install | `~/.helloagents/host-projections/claude-marketplace`, Claude Code plugin metadata/cache managed by the host |
451
+ | Gemini CLI | native extension install | `~/.helloagents/host-projections/gemini`, `~/.gemini/extensions/helloagents` |
450
452
  | Codex CLI | native local-plugin chain | `~/.agents/plugins/marketplace.json`, `~/plugins/helloagents/ -> ~/.helloagents/helloagents`, `~/.codex/plugins/cache/local-plugins/helloagents/local/ -> ~/.helloagents/helloagents`, `~/.codex/config.toml`, `~/.codex/hooks.json`, `~/.codex/helloagents -> ~/.helloagents/helloagents` |
451
453
 
452
- In global mode, HelloAGENTS now attempts the host-native install commands automatically. For Claude Code, the marketplace should be added from the Git URL so the plugin source stays on HTTPS and avoids an SSH-only clone during installation. If a host command is unavailable, run the same commands manually:
454
+ In global mode, HelloAGENTS now attempts the host-native install commands automatically. Claude Code uses the local marketplace projection, Gemini uses the local extension projection, and Codex keeps linking back to the same stable runtime root, so install, update, branch switching, mode switching, cleanup, and uninstall all refresh against one consistent runtime copy. If a host command is unavailable, run the same commands manually:
453
455
 
454
456
  ```text
455
- /plugin marketplace add https://github.com/hellowind777/helloagents.git
457
+ /plugin marketplace add "~/.helloagents/host-projections/claude-marketplace"
456
458
  /plugin install helloagents@helloagents
457
- helloagents install gemini --global
459
+ gemini extensions link "~/.helloagents/host-projections/gemini"
458
460
  ```
459
461
 
460
462
  For Claude Code, the CLI also tries the equivalent `claude plugin marketplace add ...` and `claude plugin install ...` commands. The marketplace is named `helloagents`, and the plugin is also named `helloagents`, so the install target is `helloagents@helloagents`. Restart the host CLI after a global install.
@@ -652,13 +654,14 @@ Codex is rules-file driven by default.
652
654
 
653
655
  - standby writes `~/.codex/AGENTS.md`
654
656
  - standby writes a portable managed `model_instructions_file = "~/.codex/AGENTS.md"`
655
- - standby writes a managed `notify = ["helloagents-js", "codex-notify"]` command for closeout notification
657
+ - standby writes a managed and portable `notify = ["helloagents-js", "codex-notify"]` command for closeout notification, so reinstalling, updating, or moving to another machine does not require rewriting an absolute path
656
658
  - standby writes silent Codex hooks to `~/.codex/hooks.json`
657
659
  - Codex `SessionStart` stays silent and reads the current `~/.helloagents/helloagents.json` at runtime instead of baking a config snapshot into `config.toml`, so first-turn and post-compaction settings stay current
658
660
  - install and update also sync HelloAGENTS-managed Codex hook trust state in `~/.codex/config.toml`, so Codex 0.129.0+ does not re-prompt for the managed hooks
659
661
  - that hook trust state is machine-local generated metadata derived from the current absolute `~/.codex/hooks.json` path; unlike `model_instructions_file = "~/.codex/AGENTS.md"`, it is not portable config and should be regenerated on each machine
660
662
  - standby creates `~/.codex/helloagents -> ~/.helloagents/helloagents`
661
663
  - global mode installs the native local-plugin chain, but keeps `~/.helloagents/helloagents` as the single managed runtime source by linking plugin roots, plugin cache, and `~/.codex/helloagents` back to it
664
+ - `doctor`, `cleanup`, and `uninstall` also recognize wrapped notify chains such as `--previous-notify ["helloagents-js", "codex-notify"]`, so Codex App / Computer Use wrappers do not cause false drift reports or break notify restoration
662
665
  - for Codex app/plugin discovery, `global` is the native path; `standby` remains the lighter default for explicit project work
663
666
  - cleanup removes only the HelloAGENTS-managed hook trust entries, while keeping user-owned hook state untouched
664
667
  - Codex hooks only synchronize runtime state and enforce Stop gates; they do not inject HelloAGENTS rules or route text through hook output
@@ -678,10 +681,11 @@ npm test
678
681
  The current suite covers:
679
682
 
680
683
  - install, update, cleanup, uninstall, branch switching, and mode switching
684
+ - Windows `.cmd` / `.bat` lifecycle dispatch without Node `DEP0190` warnings
681
685
  - one-shot shell and PowerShell lifecycle dispatch, plus wrapper env cleanup and mode-routing rules for install, update, cleanup, uninstall, and branch switching
682
686
  - Claude, Gemini, and Codex host integration behavior, including global-to-standby cleanup and failed native cleanup tracking
683
687
  - Codex managed `model_instructions_file`, `notify`, `hooks.json`, hook trust state, local plugin, marketplace, and cache behavior
684
- - Codex cleanup and canonical managed notify restoration rules
688
+ - Codex cleanup and canonical managed notify restoration rules, including wrapped `--previous-notify` chains
685
689
  - Codex `/goal` feature toggles, long-running route context, and goal-aware command contracts
686
690
  - `helloagents doctor`
687
691
  - project storage and `repo-shared` behavior
package/README_CN.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  **面向 AI 编码 CLI 的工作流层:技能、知识库、交付检查、更安全的配置写入,以及可恢复的执行流程。**
10
10
 
11
- [![Version](https://img.shields.io/badge/version-3.0.39-orange.svg)](./package.json)
11
+ [![Version](https://img.shields.io/badge/version-3.1.1-orange.svg)](./package.json)
12
12
  [![npm](https://img.shields.io/npm/v/helloagents.svg)](https://www.npmjs.com/package/helloagents)
13
13
  [![Node](https://img.shields.io/badge/node-%3E%3D18-339933.svg)](./package.json)
14
14
  [![Skills](https://img.shields.io/badge/skills-14-6366f1.svg)](./skills)
@@ -225,8 +225,10 @@ CLI 显式管理宿主文件:
225
225
  - `update` 刷新指定目标或全部目标
226
226
  - `cleanup` 删除受管注入和链接
227
227
  - `uninstall` 在移除包前执行对应清理
228
- - `doctor` 检查规则文件、链接、hooks、配置项、插件根目录、缓存副本和版本漂移;对 Codex 还会在可用时附带原生 `codex doctor` 结果
228
+ - `doctor` 检查规则文件、链接、hooks、配置项、插件根目录、缓存副本、版本漂移,以及 Claude / Gemini 是否真的装上了全局插件或扩展;对 Codex 还会在可用时附带原生 `codex doctor` 结果
229
+ - Codex 受管 `notify = ["helloagents-js", "codex-notify"]` 会继续保持可移植;`doctor`、`cleanup` 和 `uninstall` 也能识别 Codex App / Computer Use 使用的 `--previous-notify` 包装链
229
230
  - 单 CLI 模式记录只会在宿主安装成功后写入;如果原生全局清理失败,也会继续保留 `global` 记录,而不是悄悄叠加 standby
231
+ - Windows 下的 `.cmd` / `.bat` 生命周期调用现在统一走显式命令包装,不再出现 Node `DEP0190` shell 弃用警告
230
232
 
231
233
  ## 快速开始
232
234
 
@@ -313,7 +315,7 @@ helloagents codex goals enable
313
315
 
314
316
  当你不想依赖更新过程中的 `helloagents` 可执行文件时,用 npm 或一键脚本。`HELLOAGENTS=目标[:模式]` 中,目标支持 `all`、`claude`、`gemini`、`codex`;模式支持 `standby`、`global`。用于安装时,省略模式按 `standby` 处理;用于更新、清理、卸载和切换分支时,省略模式会原样下传,让 HelloAGENTS 先复用该 CLI 已记录或检测到的模式。如果未提供 `HELLOAGENTS`,一键安装脚本现在会保持“只装包/只升级包”的默认语义,不会自动部署任何宿主 CLI。若要安装自定义 tarball 或包规格,用 `HELLOAGENTS_PACKAGE`,不要写 `HELLOAGENTS_BRANCH`。对于已经装好的包,如需确保宿主一定刷新,优先在包命令后显式执行一次 `npm explore -g helloagents -- npm run sync-hosts -- ...`。
315
317
 
316
- 宿主配置使用稳定的 `helloagents-js` 入口和运行根目录 `~/.helloagents/helloagents`,Node 全局包路径变化不会破坏受管 hooks 或 Codex `notify`。Codex hooks 使用独立 `~/.codex/hooks.json`,不把大段配置写入 `config.toml`;Codex 全局插件根目录和插件缓存也会回链到这个稳定运行根目录。
318
+ 宿主配置使用稳定的 `helloagents-js` 入口和运行根目录 `~/.helloagents/helloagents`,Node 全局包路径变化不会破坏受管 hooks 或 Codex `notify`。Codex hooks 使用独立 `~/.codex/hooks.json`,不把大段配置写入 `config.toml`;Codex 全局插件根目录和插件缓存也会回链到这个稳定运行根目录。Claude Code 的 global 安装现在使用独立本地 marketplace 投影 `~/.helloagents/host-projections/claude-marketplace`,Gemini 的 global 扩展使用 `~/.helloagents/host-projections/gemini`,宿主专用打包链路不再污染共享运行根。
317
319
 
318
320
  #### npm 命令
319
321
 
@@ -445,16 +447,16 @@ npm uninstall -g helloagents
445
447
 
446
448
  | CLI | 安装方式 | 涉及文件 |
447
449
  |-----|----------|----------|
448
- | Claude Code | 原生插件安装 | Claude Code 插件系统管理 |
449
- | Gemini CLI | 原生扩展安装 | Gemini 扩展系统管理 |
450
+ | Claude Code | 原生插件安装 | `~/.helloagents/host-projections/claude-marketplace`,以及由 Claude Code 宿主管理的插件元数据 / 缓存 |
451
+ | Gemini CLI | 原生扩展安装 | `~/.helloagents/host-projections/gemini`、`~/.gemini/extensions/helloagents` |
450
452
  | Codex CLI | 原生本地插件流程 | `~/.agents/plugins/marketplace.json`、`~/plugins/helloagents/ -> ~/.helloagents/helloagents`、`~/.codex/plugins/cache/local-plugins/helloagents/local/ -> ~/.helloagents/helloagents`、`~/.codex/config.toml`、`~/.codex/hooks.json`、`~/.codex/helloagents -> ~/.helloagents/helloagents` |
451
453
 
452
- 全局模式下,HelloAGENTS 会自动尝试宿主原生命令。对 Claude Code,marketplace 应使用 Git URL 添加,这样插件安装阶段会继续走 HTTPS,不会落回 SSH-only clone。若宿主命令不可用,再手动执行:
454
+ 全局模式下,HelloAGENTS 会自动尝试宿主原生命令。Claude Code 走本地 marketplace 投影,Gemini 走本地 extension 投影,Codex 继续回链同一个稳定运行根,因此安装、更新、切分支、切模式、清理和卸载都会围绕同一份运行时副本刷新。若宿主命令不可用,再手动执行:
453
455
 
454
456
  ```text
455
- /plugin marketplace add https://github.com/hellowind777/helloagents.git
457
+ /plugin marketplace add "~/.helloagents/host-projections/claude-marketplace"
456
458
  /plugin install helloagents@helloagents
457
- helloagents install gemini --global
459
+ gemini extensions link "~/.helloagents/host-projections/gemini"
458
460
  ```
459
461
 
460
462
  Claude Code 会自动尝试等价的 `claude plugin marketplace add ...` 和 `claude plugin install ...` 命令。marketplace 名称和插件名称都是 `helloagents`,所以安装目标是 `helloagents@helloagents`。全局安装后需要重启宿主 CLI。
@@ -656,13 +658,14 @@ Codex 默认走规则文件驱动。
656
658
 
657
659
  - 标准模式写入 `~/.codex/AGENTS.md`
658
660
  - 标准模式写入可移植的受管 `model_instructions_file = "~/.codex/AGENTS.md"`
659
- - 标准模式写入受管 `notify = ["helloagents-js", "codex-notify"]` 命令用于收尾通知
661
+ - 标准模式写入受管且可移植的 `notify = ["helloagents-js", "codex-notify"]` 命令用于收尾通知,因此重装、更新或换电脑时都不需要改写绝对路径
660
662
  - 标准模式把静默 Codex hooks 写入 `~/.codex/hooks.json`
661
663
  - Codex 的 `SessionStart` 保持静默,并在运行时读取当前 `~/.helloagents/helloagents.json`,不会把配置快照固化进 `config.toml`,因此首次对话和上下文压缩后的设置都能保持最新
662
664
  - 安装和更新还会把 HelloAGENTS 受管的 Codex hook trust 状态同步到 `~/.codex/config.toml`,因此 Codex 0.129.0+ 不会再对这些受管 hooks 反复提示确认
663
665
  - 这些 hook trust 状态是基于当前机器 `~/.codex/hooks.json` 真实绝对路径生成的本机状态;它不同于 `model_instructions_file = "~/.codex/AGENTS.md"` 这类可移植配置,应在每台机器上重新生成
664
666
  - 标准模式创建 `~/.codex/helloagents -> ~/.helloagents/helloagents`
665
667
  - 全局模式安装原生本地插件流程,但仍把 `~/.helloagents/helloagents` 作为唯一受管运行时源;插件根目录、插件缓存和 `~/.codex/helloagents` 都会回链到它
668
+ - `doctor`、`cleanup` 和 `uninstall` 也能识别 `--previous-notify ["helloagents-js", "codex-notify"]` 这类包装后的 notify 链,因此 Codex App / Computer Use 不会再触发误报或破坏 notify 恢复
666
669
  - 如果你主要看重 Codex app / 插件发现链路,优先使用 `global`;如果你主要看重更轻量、更显式的项目工作流,保留 `standby`
667
670
  - 清理时只删除 HelloAGENTS 自己写入的 hook trust 条目,不影响用户已有的 hook 状态
668
671
  - Codex hooks 只做静默运行态同步和 Stop 门禁,不通过 hook 注入 HelloAGENTS 规则或路由说明
@@ -682,10 +685,11 @@ npm test
682
685
  当前测试覆盖:
683
686
 
684
687
  - 安装、更新、清理、卸载、分支切换和模式切换
688
+ - Windows `.cmd` / `.bat` 生命周期分发链路,且不再出现 Node `DEP0190` 警告
685
689
  - shell 与 PowerShell 一键脚本分发链路,以及包装脚本在安装、更新、清理、卸载和分支切换中的环境清理与模式传递规则
686
690
  - Claude、Gemini、Codex 的宿主集成行为,包括全局切回标准模式的清理和原生清理失败时的模式保留
687
691
  - Codex 受管 `model_instructions_file`、`notify`、`hooks.json`、hook trust 状态、本地插件、marketplace 和缓存行为
688
- - Codex 清理链路,以及受管 notify 恢复规则
692
+ - Codex 清理链路,以及包括 wrapped `--previous-notify` 在内的受管 notify 恢复规则
689
693
  - Codex `/goal` 功能开关、长程路由上下文和 goal 感知命令契约
690
694
  - `helloagents doctor`
691
695
  - 项目存储和 `repo-shared`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.39",
3
+ "version": "3.1.1",
4
4
  "description": "Quality-driven orchestration kernel for AI CLIs",
5
5
  "contextFileName": "bootstrap.md",
6
6
  "author": "HelloWind",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.39",
3
+ "version": "3.1.1",
4
4
  "type": "module",
5
5
  "description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, unified QA gates, safety guards, and notifications.",
6
6
  "author": "HelloWind",
@@ -1,15 +1,12 @@
1
- import { spawnSync } from 'node:child_process'
2
-
3
1
  import { normalizeHost } from './cli-lifecycle.mjs'
2
+ import { spawnCommandSync } from './cli-process.mjs'
4
3
 
5
4
  const DEFAULT_REPO_ARCHIVE_BASE = 'https://github.com/hellowind777/helloagents/archive/refs/heads'
6
5
 
7
6
  function runCommand(command, args) {
8
- const needsShell = process.platform === 'win32' && /\.cmd$/i.test(command)
9
- const result = spawnSync(command, args, {
7
+ const result = spawnCommandSync(command, args, {
10
8
  encoding: 'utf-8',
11
9
  errors: 'replace',
12
- shell: needsShell,
13
10
  stdio: 'inherit',
14
11
  windowsHide: true,
15
12
  })
@@ -24,6 +24,155 @@ function normalizePath(value = '') {
24
24
  return String(value || '').replace(/\\/g, '/')
25
25
  }
26
26
 
27
+ function isManagedCodexNotifyParts(parts) {
28
+ return Array.isArray(parts)
29
+ && parts.length === 2
30
+ && parts[0] === CODEX_MANAGED_NOTIFY_COMMAND
31
+ && parts[1] === 'codex-notify'
32
+ }
33
+
34
+ function extractTomlArrayLiteral(text = '') {
35
+ const source = String(text || '')
36
+ const equalsIndex = source.indexOf('=')
37
+ let quoted = false
38
+ let escaped = false
39
+ let commented = false
40
+ let depth = 0
41
+ let start = -1
42
+
43
+ for (let index = equalsIndex >= 0 ? equalsIndex + 1 : 0; index < source.length; index += 1) {
44
+ const char = source[index]
45
+
46
+ if (commented) {
47
+ if (char === '\n') commented = false
48
+ continue
49
+ }
50
+ if (escaped) {
51
+ escaped = false
52
+ continue
53
+ }
54
+ if (char === '\\' && quoted) {
55
+ escaped = true
56
+ continue
57
+ }
58
+ if (char === '"') {
59
+ quoted = !quoted
60
+ continue
61
+ }
62
+ if (quoted) continue
63
+ if (char === '#') {
64
+ commented = true
65
+ continue
66
+ }
67
+ if (char === '[') {
68
+ if (depth === 0) start = index
69
+ depth += 1
70
+ continue
71
+ }
72
+ if (char === ']' && depth > 0) {
73
+ depth -= 1
74
+ if (depth === 0 && start >= 0) return source.slice(start, index + 1)
75
+ }
76
+ }
77
+
78
+ return ''
79
+ }
80
+
81
+ function parseTomlStringArrayLiteral(literal = '') {
82
+ const source = String(literal || '').trim()
83
+ if (!source.startsWith('[') || !source.endsWith(']')) return null
84
+
85
+ const items = []
86
+ let quoted = false
87
+ let escaped = false
88
+ let tokenStart = -1
89
+
90
+ for (let index = 1; index < source.length; index += 1) {
91
+ const char = source[index]
92
+
93
+ if (quoted) {
94
+ if (escaped) {
95
+ escaped = false
96
+ continue
97
+ }
98
+ if (char === '\\') {
99
+ escaped = true
100
+ continue
101
+ }
102
+ if (char === '"') {
103
+ try {
104
+ items.push(JSON.parse(source.slice(tokenStart, index + 1)))
105
+ } catch {
106
+ return null
107
+ }
108
+ quoted = false
109
+ tokenStart = -1
110
+ }
111
+ continue
112
+ }
113
+
114
+ if (char === '#') {
115
+ while (index < source.length && source[index] !== '\n') index += 1
116
+ continue
117
+ }
118
+ if (/\s|,/.test(char)) continue
119
+ if (char === ']') return items
120
+ if (char !== '"') return null
121
+
122
+ quoted = true
123
+ tokenStart = index
124
+ }
125
+
126
+ return null
127
+ }
128
+
129
+ function analyzeNotifyCommandParts(parts = []) {
130
+ if (isManagedCodexNotifyParts(parts)) {
131
+ return {
132
+ managed: true,
133
+ shape: 'direct',
134
+ containsCodexNotify: true,
135
+ entrypoint: [...parts],
136
+ wrapper: '',
137
+ rawCommand: [...parts],
138
+ }
139
+ }
140
+
141
+ let containsCodexNotify = parts.includes('codex-notify')
142
+ for (let index = 0; index < parts.length - 1; index += 1) {
143
+ if (parts[index] !== '--previous-notify') continue
144
+
145
+ try {
146
+ const nested = JSON.parse(parts[index + 1])
147
+ if (!Array.isArray(nested) || !nested.every((entry) => typeof entry === 'string')) continue
148
+
149
+ const nestedAnalysis = analyzeNotifyCommandParts(nested)
150
+ containsCodexNotify = containsCodexNotify || nestedAnalysis.containsCodexNotify
151
+ if (!nestedAnalysis.managed) continue
152
+
153
+ return {
154
+ managed: true,
155
+ shape: 'chained',
156
+ containsCodexNotify: true,
157
+ entrypoint: [...nestedAnalysis.entrypoint],
158
+ wrapper: parts[0] || '',
159
+ rawCommand: [...parts],
160
+ }
161
+ } catch {
162
+ continue
163
+ }
164
+ }
165
+
166
+ return {
167
+ managed: false,
168
+ shape: parts.length ? 'external' : 'invalid',
169
+ containsCodexNotify,
170
+ entrypoint: [],
171
+ wrapper: '',
172
+ rawCommand: [...parts],
173
+ }
174
+ }
175
+
27
176
  function splitTomlLines(text = '') {
28
177
  return String(text || '').replace(/\r\n/g, '\n').split('\n')
29
178
  }
@@ -158,6 +307,43 @@ export function isManagedCodexNotify(line = '') {
158
307
  && value.includes(CODEX_MANAGED_NOTIFY_VALUE)
159
308
  }
160
309
 
310
+ export function analyzeCodexNotifyBlock(block = '') {
311
+ const source = String(block || '').trim()
312
+ if (!source) {
313
+ return {
314
+ exists: false,
315
+ managed: false,
316
+ containsCodexNotify: false,
317
+ shape: 'missing',
318
+ entrypoint: [],
319
+ wrapper: '',
320
+ rawCommand: [],
321
+ rawBlock: '',
322
+ }
323
+ }
324
+
325
+ const literal = extractTomlArrayLiteral(source)
326
+ const parts = literal ? parseTomlStringArrayLiteral(literal) : null
327
+ if (!parts) {
328
+ return {
329
+ exists: true,
330
+ managed: false,
331
+ containsCodexNotify: source.includes('codex-notify'),
332
+ shape: 'invalid',
333
+ entrypoint: [],
334
+ wrapper: '',
335
+ rawCommand: [],
336
+ rawBlock: source,
337
+ }
338
+ }
339
+
340
+ return {
341
+ exists: true,
342
+ ...analyzeNotifyCommandParts(parts),
343
+ rawBlock: source,
344
+ }
345
+ }
346
+
161
347
  export function isManagedCodexTuiNotifications(line = '') {
162
348
  return String(line || '').includes(CODEX_MANAGED_TUI_NOTIFICATIONS_VALUE)
163
349
  && String(line || '').includes(CODEX_MANAGED_TOML_COMMENT)
@@ -8,6 +8,7 @@ import {
8
8
  } from './cli-utils.mjs';
9
9
  import { ensureTimestampedBackup, readCodexBackup, removeCodexBackup } from './cli-codex-backup.mjs';
10
10
  import {
11
+ analyzeCodexNotifyBlock,
11
12
  CODEX_MANAGED_TOML_COMMENT,
12
13
  CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
13
14
  CODEX_PLUGIN_CONFIG_HEADER,
@@ -30,8 +31,10 @@ import {
30
31
  syncManagedCodexHookTrust,
31
32
  } from './cli-codex-hooks-state.mjs';
32
33
  import {
34
+ hasTopLevelTomlBlock,
33
35
  readTopLevelTomlLine,
34
36
  readTopLevelTomlBlock,
37
+ removeTopLevelTomlBlock,
35
38
  removeTopLevelTomlLines,
36
39
  } from './cli-toml.mjs';
37
40
  import { buildRuntimeCarrier, readCarrierSettings } from './cli-runtime-carrier.mjs';
@@ -153,9 +156,10 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
153
156
  const currentModelInstructions = readTopLevelTomlLine(toml, 'model_instructions_file');
154
157
  const currentNotify = readTopLevelTomlBlock(toml, 'notify');
155
158
  const currentCodexGoalsFeature = readCodexGoalsFeatureLine(toml);
159
+ const currentNotifyAnalysis = analyzeCodexNotifyBlock(currentNotify);
156
160
 
157
161
  const shouldRestoreModelInstructions = isManagedCodexModelInstruction(currentModelInstructions);
158
- const shouldRestoreNotify = isManagedCodexNotify(currentNotify);
162
+ const shouldRestoreNotify = currentNotifyAnalysis.managed || isManagedCodexNotify(currentNotify);
159
163
  const shouldRestoreCodexGoalsFeature = isManagedCodexGoalsFeature(currentCodexGoalsFeature);
160
164
 
161
165
  if (removePluginConfig) {
@@ -170,8 +174,10 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
170
174
  line.startsWith('model_instructions_file =') && isManagedCodexModelInstruction(line)).text;
171
175
  }
172
176
  if (shouldRestoreNotify) {
173
- toml = removeTopLevelTomlLines(toml, (line) =>
174
- line.startsWith('notify =') && isManagedCodexNotify(line)).text;
177
+ toml = hasTopLevelTomlBlock(toml, 'notify')
178
+ ? removeTopLevelTomlBlock(toml, 'notify')
179
+ : removeTopLevelTomlLines(toml, (line) =>
180
+ line.startsWith('notify =')).text;
175
181
  }
176
182
 
177
183
  const backupModelInstructions = readTopLevelTomlLine(backupToml, 'model_instructions_file');
@@ -182,7 +188,7 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
182
188
  modelInstructionsLine: shouldRestoreModelInstructions && !isManagedCodexBackupInstruction(backupModelInstructions)
183
189
  ? backupModelInstructions
184
190
  : '',
185
- notifyLine: shouldRestoreNotify && !isManagedCodexNotify(backupNotify)
191
+ notifyLine: shouldRestoreNotify && !analyzeCodexNotifyBlock(backupNotify).managed
186
192
  ? backupNotify
187
193
  : '',
188
194
  });
@@ -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
+ }
@@ -117,6 +117,10 @@ export function removeTopLevelTomlBlock(text, key) {
117
117
  return normalizeToml(`${normalized.slice(0, existing.start)}${normalized.slice(existing.end)}`);
118
118
  }
119
119
 
120
+ export function hasTopLevelTomlBlock(text, key) {
121
+ return Boolean(findTopLevelTomlBlock(text, key));
122
+ }
123
+
120
124
  export function prependTopLevelTomlBlocks(text, blocks) {
121
125
  const normalizedBlocks = blocks
122
126
  .map((block) => String(block || '').trim())