@hayasaka7/haya-pet 0.3.6 → 0.3.7

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ All notable changes to HAYA Pet are documented here. This project adheres to
7
7
  > 0.2.0 npm publish; they are listed under 0.2.1, which is the first version that
8
8
  > ships them.
9
9
 
10
+ ## [0.3.7]
11
+
12
+ ### Changed
13
+ - **The tray menu no longer shows state-only controls.** Hidden **Display Mode**
14
+ and **Attach Bubbles to Terminals** until those settings have real runtime
15
+ behavior. **Active Sessions** stays visible while session actions continue in a
16
+ separate workflow.
17
+
10
18
  ## [0.3.6]
11
19
 
12
20
  ### Fixed
@@ -38,6 +46,12 @@ All notable changes to HAYA Pet are documented here. This project adheres to
38
46
  watcher, so a resumed main rollout could be rejected before the guardian trunk
39
47
  was matched to it. The guardian watcher now uses the same fresh-mtime + wrapped
40
48
  cwd rule for resumed main sessions before following the guardian review trunk.
49
+ - **Codex hook review is one-time again.** Codex stores approved hook hashes in
50
+ the generated `$CODEX_HOME/haya-pet.config.toml` profile under `[hooks.state]`.
51
+ The injector used to rewrite the whole managed profile on every launch, deleting
52
+ that trust state and forcing Codex to ask for hook review every time. The
53
+ injector now preserves Codex's hook trust tables while regenerating the HAYA
54
+ hook definitions.
41
55
 
42
56
  ### Added
43
57
  - **`HAYA_PET_DAEMON_DEBUG` diagnostic.** When set to a file path, the companion
package/README.md CHANGED
@@ -136,7 +136,7 @@ success or failure briefly, then fades.
136
136
  | Global pet | Reacts to the highest-priority session and can be dragged anywhere. |
137
137
  | Session bubbles | One bubble per running AI session, ordered by connect time. |
138
138
  | Folder button | Folds the bubbles away when you want a cleaner desktop. |
139
- | Tray menu | Show/hide, display mode, installed pets, reset position, and quit. |
139
+ | Tray menu | Show/hide, active sessions, installed pets, reset position, updates, and quit. |
140
140
  | Resize grip | Hover the pet, drag the corner, and keep the size you like. |
141
141
 
142
142
  ## Screenshots
@@ -144,7 +144,7 @@ success or failure briefly, then fades.
144
144
  | | |
145
145
  |---|---|
146
146
  | **The global pet** - reacting to the highest-priority session.<br>![Pet overlay](docs/screenshots/pet-overlay.png) | **Session bubbles** - one per active session, with status icons.<br>![Session bubbles](docs/screenshots/session-bubbles.png) |
147
- | **Folder collapsed** - bubbles tucked away beside the pet.<br>![Folder collapsed](docs/screenshots/folder-collapsed.png) | **Tray menu** - show/hide, pets, reset position, quit.<br>![Tray menu](docs/screenshots/tray-menu.png) |
147
+ | **Folder collapsed** - bubbles tucked away beside the pet.<br>![Folder collapsed](docs/screenshots/folder-collapsed.png) | **Tray menu** - show/hide, sessions, pets, reset position, quit.<br>![Tray menu](docs/screenshots/tray-menu.png) |
148
148
 
149
149
  ## Supported Clients
150
150
 
@@ -217,7 +217,7 @@ non-observe mode keeps terminal input native.
217
217
  | Drag | Move the pet; position is saved. |
218
218
  | Drag corner grip | Resize from 0.5x to 2x; size is saved. |
219
219
  | Double-click grip | Reset to normal size. |
220
- | Tray icon | Open menu for display, sessions, pets, reset, and quit. |
220
+ | Tray icon | Open menu for sessions, pets, reset, updates, and quit. |
221
221
 
222
222
  ## Commands
223
223
 
@@ -2,13 +2,6 @@
2
2
  // converts these descriptors into a native Menu; keeping it pure makes the
3
3
  // recovery controls testable.
4
4
 
5
- const DISPLAY_MODES = Object.freeze([
6
- { value: "global", label: "Global" },
7
- { value: "cluster", label: "Cluster" },
8
- { value: "per-terminal", label: "Per Terminal" },
9
- { value: "hybrid", label: "Hybrid" }
10
- ]);
11
-
12
5
  export function buildTrayTooltip() {
13
6
  return "HAYA Pet";
14
7
  }
@@ -22,17 +15,6 @@ export function buildTrayMenu(state = {}) {
22
15
  id: "toggle_pet",
23
16
  label: state.petVisible ? "Hide Pet" : "Show Pet"
24
17
  },
25
- {
26
- id: "display_mode",
27
- label: "Display Mode",
28
- submenu: DISPLAY_MODES.map((mode) => ({
29
- id: `display_mode:${mode.value}`,
30
- label: mode.label,
31
- value: mode.value,
32
- type: "radio",
33
- checked: state.displayMode === mode.value
34
- }))
35
- },
36
18
  {
37
19
  id: "sessions",
38
20
  label: "Active Sessions",
@@ -43,17 +25,9 @@ export function buildTrayMenu(state = {}) {
43
25
  label: "Installed Pets",
44
26
  submenu: buildPetItems(pets, state.selectedPetId)
45
27
  },
46
- {
47
- id: "attach_bubbles",
48
- label: "Attach Bubbles to Terminals",
49
- type: "checkbox",
50
- checked: Boolean(state.attachBubblesToTerminals)
51
- },
52
28
  { id: "reset_position", label: "Reset Position" },
53
- // Parked until a real settings window exists: every current setting already
54
- // has a home (tray toggles, `haya-pet hooks`, drag/grip gestures), so the
55
- // item would be a dead button. Re-enable once settings outgrow the tray
56
- // (e.g. bubble text size, linger duration) and a handler is wired up.
29
+ // Parked until a real settings window exists; partially implemented knobs
30
+ // stay hidden instead of showing dead or state-only controls.
57
31
  // { id: "settings", label: "Open Settings" },
58
32
  // Only present when the daily npm update check found a newer version;
59
33
  // clicking it opens the package page (the app never runs npm itself).
@@ -13,9 +13,11 @@ const baseState = {
13
13
  test("includes the documented recovery controls", () => {
14
14
  const menu = buildTrayMenu(baseState);
15
15
  const ids = menu.map((item) => item.id);
16
- for (const id of ["toggle_pet", "display_mode", "sessions", "pets", "attach_bubbles", "reset_position", "quit"]) {
16
+ for (const id of ["toggle_pet", "sessions", "pets", "reset_position", "quit"]) {
17
17
  assert.ok(ids.includes(id), `missing ${id}`);
18
18
  }
19
+ assert.ok(!ids.includes("display_mode"), "display mode should stay hidden until implemented");
20
+ assert.ok(!ids.includes("attach_bubbles"), "attach bubbles should stay hidden until implemented");
19
21
  // "Open Settings" is parked until a settings window exists (every current
20
22
  // setting already has a tray/CLI/gesture home) — it must not be shown dead.
21
23
  assert.ok(!ids.includes("settings"), "settings item should stay hidden until implemented");
@@ -26,14 +28,6 @@ test("toggles the pet label based on visibility", () => {
26
28
  assert.equal(buildTrayMenu({ ...baseState, petVisible: false }).find((i) => i.id === "toggle_pet").label, "Show Pet");
27
29
  });
28
30
 
29
- test("checks the current display mode in the submenu", () => {
30
- const submenu = buildTrayMenu(baseState).find((i) => i.id === "display_mode").submenu;
31
- const hybrid = submenu.find((i) => i.value === "hybrid");
32
- const global = submenu.find((i) => i.value === "global");
33
- assert.equal(hybrid.checked, true);
34
- assert.equal(global.checked, false);
35
- });
36
-
37
31
  test("lists active sessions or shows an empty placeholder", () => {
38
32
  const withSessions = buildTrayMenu(baseState).find((i) => i.id === "sessions").submenu;
39
33
  assert.equal(withSessions[0].label, "Codex · netdisk-server");
@@ -42,11 +36,6 @@ test("lists active sessions or shows an empty placeholder", () => {
42
36
  assert.equal(empty[0].enabled, false);
43
37
  });
44
38
 
45
- test("reflects the attach-bubbles checkbox state", () => {
46
- assert.equal(buildTrayMenu(baseState).find((i) => i.id === "attach_bubbles").checked, true);
47
- assert.equal(buildTrayMenu({ ...baseState, attachBubblesToTerminals: false }).find((i) => i.id === "attach_bubbles").checked, false);
48
- });
49
-
50
39
  test("shows the update item only when a newer version is known", () => {
51
40
  const withoutUpdate = buildTrayMenu(baseState);
52
41
  assert.ok(!withoutUpdate.some((i) => i.id === "update"), "no update item by default");
@@ -117,6 +117,20 @@ Issues found in live use, with their current status.
117
117
  surfaces as turn-end *idle*). The TUI's passive `/approve` denial-override
118
118
  picker is not a blocking prompt.
119
119
 
120
+ ## ✅ Resolved: Codex asked to review HAYA hooks on every launch
121
+
122
+ - **Symptom:** Even after approving HAYA Pet's Codex hooks once, every new
123
+ `haya-pet run --client codex` showed Codex's hook review prompt again.
124
+ - **Root cause:** HAYA Pet correctly wrote a stable
125
+ `$CODEX_HOME/haya-pet.config.toml` profile, but Codex stores the user's hook
126
+ trust decisions back into that same profile under `[hooks.state]` as
127
+ `trusted_hash` entries. The injector rewrote the entire profile on every
128
+ launch, so it deleted Codex's trust cache before Codex could reuse it.
129
+ - **Fix:** The Codex hook injector now regenerates the HAYA-managed hook tables
130
+ while preserving the Codex-managed `[hooks.state]` tables from the existing
131
+ profile. Users may need to approve once after updating; after that, unchanged
132
+ hook commands should stay trusted.
133
+
120
134
  ## ✅ Resolved: Codex pet looked busy immediately after startup
121
135
 
122
136
  - **Symptom:** Starting a wrapped Codex session and doing nothing could still make
@@ -4,4 +4,4 @@ Place screenshot PNGs referenced by the root README here (~800px wide):
4
4
  - `pet-overlay.png` — the pet reacting to the highest-priority session
5
5
  - `session-bubbles.png` — bubbles expanded, showing per-session status icons
6
6
  - `folder-collapsed.png` — bubbles folded away beside the pet
7
- - `tray-menu.png` — the tray menu (show/hide, pets, reset position, Quit)
7
+ - `tray-menu.png` — the tray menu (show/hide, sessions, pets, reset position, Quit)
@@ -18,6 +18,7 @@ deferred problems with known root causes.
18
18
  | Typing doesn't work / **Claude Code** TUI frozen under `haya-pet run` | You have hooks enabled and Claude is showing its *review hooks* trust prompt (approve it once), or your Claude is too old for `--settings`. Run `haya-pet hooks off` (or set `HAYA_PET_NO_HOOKS=1`) for native passthrough with lifecycle-only status — typing and Shift+Tab work normally. |
19
19
  | Pet changes status after a **Claude Code** subagent finishes, even though the main agent already stopped | Fixed — Claude `SubagentStop` is ignored because it is not a reliable main-turn state. Update to the latest version and restart the wrapped Claude session so the new hook settings are used. |
20
20
  | Pet shows only **idle/lifecycle** while **Codex** works | Live status is opt-in: run `haya-pet hooks on` once (persisted, global), then `haya-pet run --client codex -- codex`; approve Codex's one-time *review hooks* prompt. `thinking`/`idle` come from hooks, `running_tool`/`editing_files` from a transcript watcher, and approval states from the `PermissionRequest` hook plus a guardian-review watcher. |
21
+ | **Codex** asks to review HAYA hooks on every launch | Fixed — update to the latest version, then approve once more. Codex writes trusted hook hashes into `$CODEX_HOME/haya-pet.config.toml` under `[hooks.state]`; HAYA Pet now preserves that Codex-managed block when refreshing the hook profile. |
21
22
  | Pet showed **waiting for approval** while **Codex** auto-reviewed the request ("Approve for me") | Fixed — with `approvals_reviewer = auto_review` (legacy `guardian_subagent`) Codex's guardian decides without asking you; the pet now reports **reviewing** from the permission hook itself, then **working** on an allow verdict or **thinking** on a deny. *Waiting for approval* still shows when Codex actually asks you (`approvals_reviewer = "user"`). Restart the wrapped Codex session after updating so Codex reloads the changed hook command. |
22
23
  | Pet shows **shell_command** or **thinking** right after starting Codex, before you prompt it | Fixed — the Codex transcript and guardian watchers now ignore rollouts whose `session_meta.timestamp` predates the current wrapper launch, so another active Codex session cannot drive this pet's status. Restart the wrapped Codex session after updating. |
23
24
  | **Codex** live status didn't turn on / you pass your own `-p`/`--profile` | Codex allows only one profile, so haya-pet skips hook injection when you supply your own and prints a notice. Drop your `-p` for that run to get live status, or accept lifecycle-only. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hayasaka7/haya-pet",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "type": "module",
5
5
  "description": "Generic AI CLI pet runtime foundation.",
6
6
  "keywords": [
@@ -8,7 +8,7 @@
8
8
  // across sessions so Codex's hook-trust review only needs approving once. fnm hands
9
9
  // out a per-shell symlink for process.execPath that dies when the launching shell
10
10
  // exits, so we realpath it before baking it into the hook command.
11
- import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
11
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
@@ -32,7 +32,8 @@ export function injectCodexHooks({ nodePath, cliPath, codexHome, env = process.e
32
32
  // rewrite identical bytes, and the hooks stay "trusted" across launches.
33
33
  mkdirSync(home, { recursive: true });
34
34
  const profilePath = join(home, PROFILE_FILE);
35
- writeFileSync(profilePath, toml, "utf8");
35
+ const trustedState = readCodexHookTrustState(profilePath);
36
+ writeFileSync(profilePath, appendCodexHookTrustState(toml, trustedState), "utf8");
36
37
 
37
38
  // The profile file is stable and reusable on purpose — leaving it in place is
38
39
  // what lets Codex remember the hooks are trusted. cleanup is a no-op kept for
@@ -47,3 +48,51 @@ function safeRealpath(target) {
47
48
  return target;
48
49
  }
49
50
  }
51
+
52
+ function readCodexHookTrustState(profilePath) {
53
+ try {
54
+ return extractCodexHookTrustState(readFileSync(profilePath, "utf8"));
55
+ } catch {
56
+ return "";
57
+ }
58
+ }
59
+
60
+ function appendCodexHookTrustState(toml, trustedState) {
61
+ if (!trustedState) {
62
+ return toml;
63
+ }
64
+ return `${toml.trimEnd()}\n\n${trustedState.trim()}\n`;
65
+ }
66
+
67
+ function extractCodexHookTrustState(toml) {
68
+ const lines = String(toml).split(/\r?\n/);
69
+ const output = [];
70
+ let inHookState = false;
71
+
72
+ for (const line of lines) {
73
+ const tableName = readTomlTableName(line);
74
+ if (tableName) {
75
+ const isHookStateTable = tableName === "hooks.state" || tableName.startsWith("hooks.state.");
76
+ if (isHookStateTable) {
77
+ inHookState = true;
78
+ } else if (inHookState) {
79
+ break;
80
+ }
81
+ }
82
+
83
+ if (inHookState) {
84
+ output.push(line);
85
+ }
86
+ }
87
+
88
+ return output.join("\n").trim();
89
+ }
90
+
91
+ function readTomlTableName(line) {
92
+ const table = /^\s*\[([^\]]+)\]\s*$/.exec(line);
93
+ if (table) {
94
+ return table[1];
95
+ }
96
+ const arrayTable = /^\s*\[\[([^\]]+)\]\]\s*$/.exec(line);
97
+ return arrayTable?.[1];
98
+ }
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { test } from "../../../test/harness.mjs";
@@ -43,3 +43,32 @@ test("injectCodexHooks honors CODEX_HOME from env and is stable across calls", (
43
43
  rmSync(home, { recursive: true, force: true });
44
44
  }
45
45
  });
46
+
47
+ test("injectCodexHooks preserves Codex hook trust state in the managed profile", () => {
48
+ const home = mkdtempSync(join(tmpdir(), "haya-codex-home-"));
49
+ try {
50
+ const first = injectCodexHooks({
51
+ nodePath: "n",
52
+ cliPath: "c",
53
+ codexHome: home
54
+ });
55
+ const trustedState = `[hooks.state]
56
+
57
+ [hooks.state.'${first.profilePath}:user_prompt_submit:0:0']
58
+ trusted_hash = "sha256:abc123"
59
+ `;
60
+ writeFileSync(first.profilePath, `${readFileSync(first.profilePath, "utf8")}\n${trustedState}`, "utf8");
61
+
62
+ injectCodexHooks({
63
+ nodePath: "n",
64
+ cliPath: "c",
65
+ codexHome: home
66
+ });
67
+
68
+ const next = readFileSync(first.profilePath, "utf8");
69
+ assert.match(next, /\[hooks\.state\]/);
70
+ assert.match(next, /trusted_hash = "sha256:abc123"/);
71
+ } finally {
72
+ rmSync(home, { recursive: true, force: true });
73
+ }
74
+ });