helloagents 3.0.23 → 3.0.25

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.23",
3
+ "version": "3.0.25",
4
4
  "description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, quality verification (Ralph Loop), 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.23",
3
+ "version": "3.0.25",
4
4
  "description": "HelloAGENTS — Quality-driven orchestration kernel for AI CLIs with intelligent routing, quality verification (Ralph Loop), 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.23-orange.svg)](./package.json)
11
+ [![Version](https://img.shields.io/badge/version-3.0.25-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)
@@ -233,7 +233,7 @@ npm install -g helloagents
233
233
  If another executable named `helloagents` already exists in your `PATH`, use the stable managed-entry alias:
234
234
 
235
235
  ```bash
236
- helloagents-js.cmd
236
+ helloagents-js
237
237
  ```
238
238
 
239
239
  By default, `postinstall` installs the package command, initializes `~/.helloagents/helloagents.json`, and syncs runtime files to `~/.helloagents/helloagents`. No host CLI is deployed unless you set `HELLOAGENTS=target[:mode]`, such as `HELLOAGENTS=codex:global`.
@@ -313,7 +313,7 @@ If you omit `--standby` or `--global`, HelloAGENTS first reuses the tracked/dete
313
313
 
314
314
  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`, and defaults to `standby`.
315
315
 
316
- Host configs use the stable `helloagents-js.cmd` 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`.
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.
317
317
 
318
318
  #### npm commands
319
319
 
@@ -406,6 +406,8 @@ $env:HELLOAGENTS="codex:standby"; $env:HELLOAGENTS_ACTION="cleanup"; irm https:/
406
406
  $env:HELLOAGENTS="gemini"; $env:HELLOAGENTS_ACTION="uninstall"; irm https://raw.githubusercontent.com/hellowind777/helloagents/main/install.ps1 | iex
407
407
  ```
408
408
 
409
+ The PowerShell wrapper now forwards the same npm arguments as `install.sh`, so install, update, cleanup, uninstall, and `switch-branch` stay on the same lifecycle path.
410
+
409
411
  ### Branch switching
410
412
 
411
413
  `switch-branch` installs the requested npm/GitHub ref first, then syncs host CLIs through npm scripts so it does not depend on the `helloagents` executable during updates:
@@ -439,7 +441,7 @@ npm uninstall -g helloagents
439
441
  |-----|----------------|----------------|
440
442
  | Claude Code | native plugin install | managed by Claude Code plugin system |
441
443
  | Gemini CLI | native extension install | managed by Gemini extension system |
442
- | Codex CLI | native local-plugin chain | `~/.agents/plugins/marketplace.json`, `~/plugins/helloagents/`, `~/.codex/plugins/cache/local-plugins/helloagents/local/`, `~/.codex/config.toml`, `~/.codex/hooks.json`, `~/.codex/helloagents -> ~/plugins/helloagents` |
444
+ | 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` |
443
445
 
444
446
  In global mode, HelloAGENTS now attempts the host-native install commands automatically. If a host command is unavailable, run the same commands manually:
445
447
 
@@ -641,10 +643,13 @@ Codex is rules-file driven by default.
641
643
 
642
644
  - standby writes `~/.codex/AGENTS.md`
643
645
  - standby writes a managed `model_instructions_file = "~/.codex/AGENTS.md"`
644
- - standby writes a managed `notify = ["helloagents-js.cmd", "codex-notify"]` command for closeout notification
646
+ - standby writes a managed `notify = ["helloagents-js", "codex-notify"]` command for closeout notification
645
647
  - standby writes silent Codex hooks to `~/.codex/hooks.json`
648
+ - 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
649
+ - 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
646
650
  - standby creates `~/.codex/helloagents -> ~/.helloagents/helloagents`
647
- - global mode installs the native local-plugin chain and also loads silent hooks from `~/.codex/hooks.json`
651
+ - 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
652
+ - cleanup removes only the HelloAGENTS-managed hook trust entries and legacy managed notify residues, while keeping user-owned hook state untouched
648
653
  - Codex hooks only synchronize runtime state and enforce Stop gates; they do not inject HelloAGENTS rules or route text through hook output
649
654
  - Codex closeout de-duplicates Stop hooks and native `codex-notify`, so one turn does not notify twice
650
655
  - `/goal` remains Codex-native. Enable it explicitly with `helloagents codex goals enable` when long-running plan execution is needed
@@ -658,11 +663,13 @@ Run all tests:
658
663
  npm test
659
664
  ```
660
665
 
661
- The current suite includes 107 tests and covers:
666
+ The current suite includes 116 tests and covers:
662
667
 
663
668
  - install, update, uninstall, cleanup, and mode switching
669
+ - one-shot PowerShell lifecycle dispatch for install, update, cleanup, uninstall, and branch switching
664
670
  - Claude, Gemini, and Codex config merge and restore behavior
665
- - Codex managed `model_instructions_file`, `notify`, `hooks.json`, local plugin, marketplace, and cache behavior
671
+ - Codex managed `model_instructions_file`, `notify`, `hooks.json`, hook trust state, local plugin, marketplace, and cache behavior
672
+ - Codex cleanup of legacy managed notify variants on Windows and canonical managed notify restoration rules
666
673
  - Codex `/goal` feature toggles, long-running route context, and goal-aware command contracts
667
674
  - `helloagents doctor`
668
675
  - 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.23-orange.svg)](./package.json)
11
+ [![Version](https://img.shields.io/badge/version-3.0.25-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)
@@ -233,7 +233,7 @@ npm install -g helloagents
233
233
  如果系统里已经有别的 `helloagents` 可执行文件,可以使用稳定的受管入口别名:
234
234
 
235
235
  ```bash
236
- helloagents-js.cmd
236
+ helloagents-js
237
237
  ```
238
238
 
239
239
  默认情况下,`postinstall` 会安装包命令、初始化 `~/.helloagents/helloagents.json`,并把运行时文件同步到 `~/.helloagents/helloagents`。如果希望 npm 在安装或更新后直接部署,设置 `HELLOAGENTS=目标[:模式]`,例如 `HELLOAGENTS=codex:global`。
@@ -313,7 +313,7 @@ helloagents codex goals enable
313
313
 
314
314
  当你不想依赖更新过程中的 `helloagents` 可执行文件时,用 npm 或一键脚本。`HELLOAGENTS=目标[:模式]` 中,目标支持 `all`、`claude`、`gemini`、`codex`;模式支持 `standby`、`global`,省略时默认 `standby`。
315
315
 
316
- 宿主配置使用稳定的 `helloagents-js.cmd` 入口和运行根目录 `~/.helloagents/helloagents`,Node 全局包路径变化不会破坏受管 hooks 或 Codex `notify`。Codex hooks 使用独立 `~/.codex/hooks.json`,不把大段配置写入 `config.toml`。
316
+ 宿主配置使用稳定的 `helloagents-js` 入口和运行根目录 `~/.helloagents/helloagents`,Node 全局包路径变化不会破坏受管 hooks 或 Codex `notify`。Codex hooks 使用独立 `~/.codex/hooks.json`,不把大段配置写入 `config.toml`;Codex 全局插件根目录和插件缓存也会回链到这个稳定运行根目录。
317
317
 
318
318
  #### npm 命令
319
319
 
@@ -406,6 +406,8 @@ $env:HELLOAGENTS="codex:standby"; $env:HELLOAGENTS_ACTION="cleanup"; irm https:/
406
406
  $env:HELLOAGENTS="gemini"; $env:HELLOAGENTS_ACTION="uninstall"; irm https://raw.githubusercontent.com/hellowind777/helloagents/main/install.ps1 | iex
407
407
  ```
408
408
 
409
+ PowerShell 包装脚本现在会传递与 `install.sh` 相同的 npm 参数,因此安装、更新、清理、卸载和 `switch-branch` 走的是同一条生命周期链路。
410
+
409
411
  ### 分支切换
410
412
 
411
413
  `switch-branch` 会先安装指定 npm/GitHub ref,再通过 npm 脚本同步宿主 CLI,避免依赖更新过程中的 `helloagents` 可执行文件:
@@ -439,7 +441,7 @@ npm uninstall -g helloagents
439
441
  |-----|----------|----------|
440
442
  | Claude Code | 原生插件安装 | 由 Claude Code 插件系统管理 |
441
443
  | Gemini CLI | 原生扩展安装 | 由 Gemini 扩展系统管理 |
442
- | Codex CLI | 原生本地插件流程 | `~/.agents/plugins/marketplace.json`、`~/plugins/helloagents/`、`~/.codex/plugins/cache/local-plugins/helloagents/local/`、`~/.codex/config.toml`、`~/.codex/hooks.json`、`~/.codex/helloagents -> ~/plugins/helloagents` |
444
+ | 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` |
443
445
 
444
446
  全局模式下,HelloAGENTS 会自动尝试宿主原生命令。若宿主命令不可用,再手动执行:
445
447
 
@@ -643,10 +645,13 @@ Codex 默认走规则文件驱动。
643
645
 
644
646
  - 标准模式写入 `~/.codex/AGENTS.md`
645
647
  - 标准模式写入受管 `model_instructions_file = "~/.codex/AGENTS.md"`
646
- - 标准模式写入受管 `notify = ["helloagents-js.cmd", "codex-notify"]` 命令用于收尾通知
648
+ - 标准模式写入受管 `notify = ["helloagents-js", "codex-notify"]` 命令用于收尾通知
647
649
  - 标准模式把静默 Codex hooks 写入 `~/.codex/hooks.json`
650
+ - Codex 的 `SessionStart` 保持静默,并在运行时读取当前 `~/.helloagents/helloagents.json`,不会把配置快照固化进 `config.toml`,因此首次对话和上下文压缩后的设置都能保持最新
651
+ - 安装和更新还会把 HelloAGENTS 受管的 Codex hook trust 状态同步到 `~/.codex/config.toml`,因此 Codex 0.129.0+ 不会再对这些受管 hooks 反复提示确认
648
652
  - 标准模式创建 `~/.codex/helloagents -> ~/.helloagents/helloagents`
649
- - 全局模式安装原生本地插件流程,并同样用 `~/.codex/hooks.json` 加载静默 hooks
653
+ - 全局模式安装原生本地插件流程,但仍把 `~/.helloagents/helloagents` 作为唯一受管运行时源;插件根目录、插件缓存和 `~/.codex/helloagents` 都会回链到它
654
+ - 清理时只删除 HelloAGENTS 自己写入的 hook trust 条目和旧式受管 notify 残留,不影响用户已有的 hook 状态
650
655
  - Codex hooks 只做静默运行态同步和 Stop 门禁,不通过 hook 注入 HelloAGENTS 规则或路由说明
651
656
  - Codex 收尾会对 Stop hook 和原生 `codex-notify` 去重,避免同一轮重复通知
652
657
  - `/goal` 保持 Codex 原生能力;需要长程执行时,用 `helloagents codex goals enable` 显式启用
@@ -660,11 +665,13 @@ Codex 默认走规则文件驱动。
660
665
  npm test
661
666
  ```
662
667
 
663
- 当前测试共 107 项,覆盖:
668
+ 当前测试共 116 项,覆盖:
664
669
 
665
670
  - 安装、更新、卸载、清理和模式切换
671
+ - PowerShell 一键脚本的安装、更新、清理、卸载和分支切换分发链路
666
672
  - Claude、Gemini、Codex 的配置合并与恢复
667
- - Codex 受管 `model_instructions_file`、`notify`、`hooks.json`、本地插件、marketplace 和缓存行为
673
+ - Codex 受管 `model_instructions_file`、`notify`、`hooks.json`、hook trust 状态、本地插件、marketplace 和缓存行为
674
+ - Windows 下 Codex 旧式受管 notify 变体的清理,以及受管 notify 恢复规则
668
675
  - Codex `/goal` 功能开关、长程路由上下文和 goal 感知命令契约
669
676
  - `helloagents doctor`
670
677
  - 项目存储和 `repo-shared`
package/bootstrap-lite.md CHANGED
@@ -6,7 +6,7 @@
6
6
  配置文件: ~/.helloagents/helloagents.json
7
7
  `output_language` 非空时,所有用户可见文本使用该语言;为空则跟随用户当前语言。
8
8
  会话级缓存优先:当前上下文已有"当前用户设置"、原始 JSON 或读取摘要,且覆盖所需配置项时,直接复用。
9
- 仅在缺少所需项、用户要求刷新,或本轮修改后需要核验时读取;输出格式只在缺少 `output_format` 已知值时触发读取。
9
+ 仅在缺少所需项、用户要求刷新,或本轮修改后需要核验时读取;对 Codex 来说,首次对话前若当前上下文仍缺少所需配置项,必须先读取一次 `~/.helloagents/helloagents.json`,压缩/恢复后的首次对话同样先重读一次;输出格式只在缺少 `output_format` 已知值时触发读取。
10
10
  同一会话内,同一路径的配置文件、模块、SKILL、模板只读一次并跨轮复用;读取失败必须明示,并按默认值或已知设置执行。
11
11
 
12
12
  ## 编码原则
package/bootstrap.md CHANGED
@@ -6,7 +6,7 @@
6
6
  配置文件: ~/.helloagents/helloagents.json
7
7
  `output_language` 非空时,所有用户可见文本使用该语言;为空则跟随用户当前语言。
8
8
  会话级缓存优先:当前上下文已有"当前用户设置"、原始 JSON 或读取摘要,且覆盖所需配置项时,直接复用。
9
- 仅在缺少所需项、用户要求刷新,或本轮修改后需要核验时读取;输出格式只在缺少 `output_format` 已知值时触发读取。
9
+ 仅在缺少所需项、用户要求刷新,或本轮修改后需要核验时读取;对 Codex 来说,首次对话前若当前上下文仍缺少所需配置项,必须先读取一次 `~/.helloagents/helloagents.json`,压缩/恢复后的首次对话同样先重读一次;输出格式只在缺少 `output_format` 已知值时触发读取。
10
10
  同一会话内,同一路径的配置文件、模块、SKILL、模板只读一次并跨轮复用;读取失败必须明示,并按默认值或已知设置执行。
11
11
 
12
12
  ## 编码原则
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.23",
3
+ "version": "3.0.25",
4
4
  "description": "Quality-driven orchestration kernel for AI CLIs",
5
5
  "contextFileName": "bootstrap.md",
6
6
  "author": "HelloWind",
package/install.ps1 CHANGED
@@ -47,10 +47,10 @@ if (-not $Package) {
47
47
  }
48
48
 
49
49
  function Invoke-Npm {
50
- param([string[]]$Args)
51
- & npm @Args
50
+ param([string[]]$NpmArgs)
51
+ & npm @NpmArgs
52
52
  if ($LASTEXITCODE -ne 0) {
53
- throw "npm $($Args -join ' ') failed with exit code $LASTEXITCODE"
53
+ throw "npm $($NpmArgs -join ' ') failed with exit code $LASTEXITCODE"
54
54
  }
55
55
  }
56
56
 
@@ -63,9 +63,9 @@ function Enable-PostinstallDeploy {
63
63
  function Invoke-HostScript {
64
64
  param([string]$ScriptName)
65
65
  if ($Target -eq "all") {
66
- Invoke-Npm @("explore", "-g", "helloagents", "--", "npm", "run", $ScriptName, "--", "--all", "--$Mode")
66
+ Invoke-Npm -NpmArgs @("explore", "-g", "helloagents", "--", "npm", "run", $ScriptName, "--", "--all", "--$Mode")
67
67
  } else {
68
- Invoke-Npm @("explore", "-g", "helloagents", "--", "npm", "run", $ScriptName, "--", $Target, "--$Mode")
68
+ Invoke-Npm -NpmArgs @("explore", "-g", "helloagents", "--", "npm", "run", $ScriptName, "--", $Target, "--$Mode")
69
69
  }
70
70
  }
71
71
 
@@ -84,15 +84,15 @@ function Uninstall-Hosts {
84
84
  switch ($Action) {
85
85
  "install" {
86
86
  Enable-PostinstallDeploy
87
- Invoke-Npm @("install", "-g", $Package)
87
+ Invoke-Npm -NpmArgs @("install", "-g", $Package)
88
88
  }
89
89
  "update" {
90
90
  if ($Branch -or $env:HELLOAGENTS_PACKAGE) {
91
- Invoke-Npm @("install", "-g", $Package)
91
+ Invoke-Npm -NpmArgs @("install", "-g", $Package)
92
92
  } else {
93
93
  & npm update -g helloagents
94
94
  if ($LASTEXITCODE -ne 0) {
95
- Invoke-Npm @("install", "-g", "helloagents")
95
+ Invoke-Npm -NpmArgs @("install", "-g", "helloagents")
96
96
  }
97
97
  }
98
98
  Sync-Hosts
@@ -104,14 +104,14 @@ switch ($Action) {
104
104
  if (-not $Branch -and -not $env:HELLOAGENTS_PACKAGE) {
105
105
  throw "HELLOAGENTS_BRANCH or HELLOAGENTS_PACKAGE is required for switch-branch"
106
106
  }
107
- Invoke-Npm @("install", "-g", $Package)
107
+ Invoke-Npm -NpmArgs @("install", "-g", $Package)
108
108
  Sync-Hosts
109
109
  }
110
110
  "branch" {
111
111
  if (-not $Branch -and -not $env:HELLOAGENTS_PACKAGE) {
112
112
  throw "HELLOAGENTS_BRANCH or HELLOAGENTS_PACKAGE is required for branch"
113
113
  }
114
- Invoke-Npm @("install", "-g", $Package)
114
+ Invoke-Npm -NpmArgs @("install", "-g", $Package)
115
115
  Sync-Hosts
116
116
  }
117
117
  "uninstall" {
@@ -120,7 +120,7 @@ switch ($Action) {
120
120
  } catch {
121
121
  Write-Warning "Failed to cleanup HelloAGENTS host integrations before uninstall: $_"
122
122
  }
123
- Invoke-Npm @("uninstall", "-g", "helloagents")
123
+ Invoke-Npm -NpmArgs @("uninstall", "-g", "helloagents")
124
124
  }
125
125
  default {
126
126
  throw "Unsupported HELLOAGENTS_ACTION: $Action"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helloagents",
3
- "version": "3.0.23",
3
+ "version": "3.0.25",
4
4
  "type": "module",
5
5
  "description": "HelloAGENTS — The orchestration kernel that makes any AI CLI smarter. Adds intelligent routing, quality verification (Ralph Loop), safety guards, and notifications.",
6
6
  "author": "HelloWind",
@@ -8,15 +8,22 @@ import {
8
8
 
9
9
  export const CODEX_PLUGIN_CONFIG_HEADER = '[plugins."helloagents@local-plugins"]'
10
10
  export const CODEX_FEATURES_HEADER = '[features]'
11
+ export const CODEX_TUI_HEADER = '[tui]'
11
12
  export const CODEX_MANAGED_TOML_COMMENT = '# helloagents-managed'
12
13
  export const CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH = '~/.codex/AGENTS.md'
13
- export const CODEX_MANAGED_NOTIFY_COMMAND = 'helloagents-js.cmd'
14
+ export const CODEX_MANAGED_NOTIFY_COMMAND = 'helloagents-js'
14
15
  export const CODEX_MANAGED_NOTIFY_VALUE = `["${CODEX_MANAGED_NOTIFY_COMMAND}", "codex-notify"]`
16
+ const CODEX_MANAGED_NOTIFY_LEGACY_VALUES = [
17
+ `["${CODEX_MANAGED_NOTIFY_COMMAND}.cmd", "codex-notify"]`,
18
+ `["${CODEX_MANAGED_NOTIFY_COMMAND}.exe", "codex-notify"]`,
19
+ ]
20
+ export const CODEX_MANAGED_TUI_NOTIFICATIONS_VALUE = '["plan-mode-prompt"]'
15
21
  export const CODEX_HOOKS_FEATURE_KEY = 'hooks'
16
22
  export const CODEX_LEGACY_HOOKS_FEATURE_KEY = 'codex_hooks'
17
23
  export const CODEX_GOALS_FEATURE_KEY = 'goals'
18
24
  export const CODEX_MANAGED_GOALS_FEATURE_LINE = `${CODEX_GOALS_FEATURE_KEY} = true ${CODEX_MANAGED_TOML_COMMENT}`
19
25
  export const CODEX_MANAGED_GOALS_DISABLED_LINE = `${CODEX_GOALS_FEATURE_KEY} = false ${CODEX_MANAGED_TOML_COMMENT}`
26
+ export const CODEX_MANAGED_TUI_NOTIFICATIONS_LINE = `notifications = ${CODEX_MANAGED_TUI_NOTIFICATIONS_VALUE} ${CODEX_MANAGED_TOML_COMMENT}`
20
27
 
21
28
  function normalizePath(value = '') {
22
29
  return String(value || '').replace(/\\/g, '/')
@@ -106,9 +113,17 @@ export function readCodexGoalsFeatureLine(text) {
106
113
  return readCodexFeatureLine(text, CODEX_GOALS_FEATURE_KEY)
107
114
  }
108
115
 
116
+ export function readCodexTuiNotificationsLine(text) {
117
+ return readCodexSectionLine(text, CODEX_TUI_HEADER, 'notifications')
118
+ }
119
+
109
120
  export function readCodexFeatureLine(text, key) {
121
+ return readCodexSectionLine(text, CODEX_FEATURES_HEADER, key)
122
+ }
123
+
124
+ export function readCodexSectionLine(text, headerLine, key) {
110
125
  const lines = splitTomlLines(text)
111
- const bounds = findSectionBounds(lines, CODEX_FEATURES_HEADER)
126
+ const bounds = findSectionBounds(lines, headerLine)
112
127
  const keyIndex = findSectionKeyIndex(lines, bounds, key)
113
128
  return keyIndex >= 0 ? lines[keyIndex].trim() : ''
114
129
  }
@@ -157,7 +172,16 @@ export function isManagedCodexModelInstruction(line = '') {
157
172
 
158
173
  export function isManagedCodexNotify(line = '') {
159
174
  const value = String(line || '').replace(/\\/g, '/')
160
- return value.includes(CODEX_MANAGED_NOTIFY_VALUE)
175
+ return value.includes(CODEX_MANAGED_TOML_COMMENT)
176
+ && (
177
+ value.includes(CODEX_MANAGED_NOTIFY_VALUE)
178
+ || CODEX_MANAGED_NOTIFY_LEGACY_VALUES.some((entry) => value.includes(entry))
179
+ )
180
+ }
181
+
182
+ export function isManagedCodexTuiNotifications(line = '') {
183
+ return String(line || '').includes(CODEX_MANAGED_TUI_NOTIFICATIONS_VALUE)
184
+ && String(line || '').includes(CODEX_MANAGED_TOML_COMMENT)
161
185
  }
162
186
 
163
187
  function isManagedFeatureLine(line = '', key = '') {
@@ -223,6 +247,29 @@ export function installCodexManagedTopLevelConfig(toml, { modelInstructionsPath
223
247
  ])
224
248
  }
225
249
 
250
+ export function installCodexManagedTuiConfig(text) {
251
+ const currentLine = readCodexTuiNotificationsLine(text)
252
+ if (currentLine && !isManagedCodexTuiNotifications(currentLine)) {
253
+ return normalizeToml(text)
254
+ }
255
+
256
+ return upsertTomlSectionLine(
257
+ text,
258
+ CODEX_TUI_HEADER,
259
+ 'notifications',
260
+ CODEX_MANAGED_TUI_NOTIFICATIONS_LINE,
261
+ )
262
+ }
263
+
264
+ export function removeCodexManagedTuiConfig(text) {
265
+ return removeTomlSectionLine(
266
+ text,
267
+ CODEX_TUI_HEADER,
268
+ 'notifications',
269
+ isManagedCodexTuiNotifications,
270
+ )
271
+ }
272
+
226
273
  export function restoreCodexTopLevelConfig(toml, { modelInstructionsLine = '', notifyLine = '' }) {
227
274
  return upsertOrderedCodexTopLevelLines(toml, [
228
275
  modelInstructionsLine,
@@ -0,0 +1,264 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ import { isTomlTableHeader, normalizeToml } from './cli-toml.mjs'
4
+ import { removeIfExists, safeJson, safeRead, safeWrite } from './cli-utils.mjs'
5
+
6
+ const MANAGED_MARKER = '# helloagents-managed'
7
+
8
+ const HOOK_EVENT_KEY = {
9
+ PreToolUse: 'pre_tool_use',
10
+ PermissionRequest: 'permission_request',
11
+ PostToolUse: 'post_tool_use',
12
+ PreCompact: 'pre_compact',
13
+ PostCompact: 'post_compact',
14
+ SessionStart: 'session_start',
15
+ UserPromptSubmit: 'user_prompt_submit',
16
+ Stop: 'stop',
17
+ }
18
+
19
+ const EVENTS_WITH_MATCHER = new Set([
20
+ 'PreToolUse',
21
+ 'PermissionRequest',
22
+ 'PostToolUse',
23
+ 'PreCompact',
24
+ 'PostCompact',
25
+ 'SessionStart',
26
+ ])
27
+
28
+ const HOOK_STATE_HEADER_RE = /^\[hooks\.state\."((?:\\.|[^"])*)"\](?:\s*#.*)?$/
29
+
30
+ function normalizeLineEndings(text = '') {
31
+ return String(text || '').replace(/\r\n/g, '\n')
32
+ }
33
+
34
+ function escapeTomlBasicString(value = '') {
35
+ return String(value || '')
36
+ .replace(/\\/g, '\\\\')
37
+ .replace(/"/g, '\\"')
38
+ }
39
+
40
+ function unescapeTomlBasicString(value = '') {
41
+ return String(value || '')
42
+ .replace(/\\"/g, '"')
43
+ .replace(/\\\\/g, '\\')
44
+ }
45
+
46
+ function canonicalizeJson(value) {
47
+ if (Array.isArray(value)) return value.map(canonicalizeJson)
48
+ if (!value || typeof value !== 'object') return value
49
+
50
+ return Object.keys(value)
51
+ .sort()
52
+ .reduce((acc, key) => {
53
+ if (value[key] !== undefined) acc[key] = canonicalizeJson(value[key])
54
+ return acc
55
+ }, {})
56
+ }
57
+
58
+ function hashNormalizedHookIdentity(identity) {
59
+ const serialized = JSON.stringify(canonicalizeJson(identity))
60
+ return `sha256:${createHash('sha256').update(serialized).digest('hex')}`
61
+ }
62
+
63
+ function normalizeHookMatcher(eventName, matcher) {
64
+ if (!EVENTS_WITH_MATCHER.has(eventName)) return undefined
65
+ return matcher === undefined ? undefined : String(matcher)
66
+ }
67
+
68
+ function normalizeHookTimeout(timeout) {
69
+ const value = Number(timeout)
70
+ if (!Number.isFinite(value)) return 600
71
+ return Math.max(1, Math.trunc(value))
72
+ }
73
+
74
+ function buildHookDescriptor(eventName, group, handler) {
75
+ return JSON.stringify({
76
+ eventName,
77
+ matcher: normalizeHookMatcher(eventName, group?.matcher),
78
+ command: handler?.command || '',
79
+ })
80
+ }
81
+
82
+ function buildNormalizedHookIdentity(eventName, group, handler) {
83
+ const matcher = normalizeHookMatcher(eventName, group?.matcher)
84
+ const statusMessage = typeof handler?.statusMessage === 'string'
85
+ ? handler.statusMessage
86
+ : undefined
87
+
88
+ return {
89
+ event_name: HOOK_EVENT_KEY[eventName],
90
+ ...(matcher !== undefined ? { matcher } : {}),
91
+ hooks: [
92
+ {
93
+ type: 'command',
94
+ command: String(handler?.command || ''),
95
+ timeout: normalizeHookTimeout(handler?.timeout),
96
+ async: Boolean(handler?.async),
97
+ ...(statusMessage !== undefined ? { statusMessage } : {}),
98
+ },
99
+ ],
100
+ }
101
+ }
102
+
103
+ function isHelloagentsCommandHandler(handler) {
104
+ return handler?.type === 'command'
105
+ && typeof handler.command === 'string'
106
+ && handler.command.includes('helloagents')
107
+ }
108
+
109
+ function serializeHookStateBlock(entry) {
110
+ const lines = [`[hooks.state."${escapeTomlBasicString(entry.key)}"]`]
111
+ if (entry.enabled === false) lines.push('enabled = false')
112
+ lines.push(`trusted_hash = "${escapeTomlBasicString(entry.trustedHash)}" ${MANAGED_MARKER}`)
113
+ return lines.join('\n')
114
+ }
115
+
116
+ function collectHookStateSections(text = '') {
117
+ const lines = normalizeLineEndings(text).split('\n')
118
+ const sections = []
119
+
120
+ for (let index = 0; index < lines.length; index += 1) {
121
+ const match = HOOK_STATE_HEADER_RE.exec(lines[index].trim())
122
+ if (!match) continue
123
+
124
+ let end = lines.length
125
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
126
+ if (isTomlTableHeader(lines[cursor])) {
127
+ end = cursor
128
+ break
129
+ }
130
+ }
131
+
132
+ const bodyLines = lines.slice(index + 1, end)
133
+ const enabledLine = bodyLines.find((line) => /^\s*enabled\s*=/.test(line))
134
+ const trustedHashLine = bodyLines.find((line) => /^\s*trusted_hash\s*=/.test(line))
135
+ const trustedHashMatch = trustedHashLine?.match(/^\s*trusted_hash\s*=\s*"((?:\\.|[^"])*)"/)
136
+
137
+ sections.push({
138
+ key: unescapeTomlBasicString(match[1]),
139
+ start: index,
140
+ end,
141
+ enabled: /^\s*enabled\s*=\s*false\b/.test(enabledLine || '') ? false : undefined,
142
+ trustedHash: trustedHashMatch ? unescapeTomlBasicString(trustedHashMatch[1]) : '',
143
+ managed: lines[index].includes(MANAGED_MARKER)
144
+ || bodyLines.some((line) => line.includes(MANAGED_MARKER)),
145
+ })
146
+
147
+ index = end - 1
148
+ }
149
+
150
+ return { lines, sections }
151
+ }
152
+
153
+ function removeHookStateSections(text, shouldRemove) {
154
+ const { lines, sections } = collectHookStateSections(text)
155
+ if (!sections.length) return normalizeToml(text)
156
+
157
+ const removedStarts = new Set(
158
+ sections
159
+ .filter(shouldRemove)
160
+ .map((section) => section.start),
161
+ )
162
+
163
+ if (!removedStarts.size) return normalizeToml(text)
164
+
165
+ const kept = []
166
+ for (let index = 0; index < lines.length;) {
167
+ const section = sections.find((item) => item.start === index)
168
+ if (!section) {
169
+ kept.push(lines[index])
170
+ index += 1
171
+ continue
172
+ }
173
+ if (!removedStarts.has(section.start)) {
174
+ kept.push(...lines.slice(section.start, section.end))
175
+ }
176
+ index = section.end
177
+ }
178
+
179
+ return normalizeToml(kept.join('\n'))
180
+ }
181
+
182
+ function appendHookStateBlocks(text, entries) {
183
+ if (!entries.length) return normalizeToml(text)
184
+ const blocks = entries.map(serializeHookStateBlock).join('\n\n')
185
+ const base = normalizeLineEndings(text).trimEnd()
186
+ return normalizeToml(base ? `${base}\n\n${blocks}` : blocks)
187
+ }
188
+
189
+ export function buildManagedCodexHookTrustEntries(hooksPath, hooksData = safeJson(hooksPath)) {
190
+ const hooks = hooksData?.hooks
191
+ if (!hooks || typeof hooks !== 'object') return []
192
+
193
+ const entries = []
194
+ for (const eventName of Object.keys(HOOK_EVENT_KEY)) {
195
+ const groups = hooks[eventName]
196
+ if (!Array.isArray(groups)) continue
197
+
198
+ groups.forEach((group, groupIndex) => {
199
+ const handlers = Array.isArray(group?.hooks) ? group.hooks : []
200
+ handlers.forEach((handler, handlerIndex) => {
201
+ if (!isHelloagentsCommandHandler(handler)) return
202
+
203
+ const key = `${hooksPath}:${HOOK_EVENT_KEY[eventName]}:${groupIndex}:${handlerIndex}`
204
+ entries.push({
205
+ key,
206
+ trustedHash: hashNormalizedHookIdentity(
207
+ buildNormalizedHookIdentity(eventName, group, handler),
208
+ ),
209
+ descriptor: buildHookDescriptor(eventName, group, handler),
210
+ })
211
+ })
212
+ })
213
+ }
214
+
215
+ return entries
216
+ }
217
+
218
+ export function readCodexHookStateSections(text = '') {
219
+ return collectHookStateSections(text).sections
220
+ }
221
+
222
+ export function syncManagedCodexHookTrust(configPath, hooksPath, hooksData = safeJson(hooksPath)) {
223
+ const entries = buildManagedCodexHookTrustEntries(hooksPath, hooksData)
224
+ if (!entries.length) return cleanupManagedCodexHookTrust(configPath)
225
+
226
+ const keySet = new Set(entries.map((entry) => entry.key))
227
+ const existingText = safeRead(configPath) || ''
228
+ const existingSections = readCodexHookStateSections(existingText)
229
+ const enabledByDescriptor = new Map()
230
+
231
+ for (const section of existingSections) {
232
+ if (!keySet.has(section.key) || section.enabled !== false) continue
233
+ const matchingEntry = entries.find((entry) => entry.key === section.key)
234
+ if (matchingEntry) enabledByDescriptor.set(matchingEntry.descriptor, false)
235
+ }
236
+
237
+ const cleanedText = removeHookStateSections(
238
+ existingText,
239
+ (section) => section.managed || keySet.has(section.key),
240
+ )
241
+
242
+ const nextEntries = entries.map((entry) => ({
243
+ ...entry,
244
+ enabled: enabledByDescriptor.get(entry.descriptor),
245
+ }))
246
+
247
+ const nextText = appendHookStateBlocks(cleanedText, nextEntries)
248
+ if (normalizeLineEndings(nextText) === normalizeLineEndings(existingText)) return false
249
+
250
+ safeWrite(configPath, nextText)
251
+ return true
252
+ }
253
+
254
+ export function cleanupManagedCodexHookTrust(configPath) {
255
+ const existingText = safeRead(configPath)
256
+ if (!existingText) return false
257
+
258
+ const nextText = removeHookStateSections(existingText, (section) => section.managed)
259
+ if (normalizeLineEndings(nextText) === normalizeLineEndings(existingText)) return false
260
+
261
+ if (nextText.trim()) safeWrite(configPath, nextText)
262
+ else removeIfExists(configPath)
263
+ return true
264
+ }