@hayasaka7/haya-pet 0.1.0 → 0.2.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.
- package/CHANGELOG.md +94 -0
- package/README.md +59 -17
- package/apps/cli/src/haya-pet.js +246 -5
- package/apps/cli/test/haya-pet.test.mjs +269 -4
- package/apps/companion/package.json +1 -1
- package/apps/companion/src/main/index.js +40 -1
- package/apps/companion/test/position-store.test.mjs +2 -1
- package/docs/architecture.md +84 -7
- package/docs/cross-os-qa.md +72 -0
- package/docs/known-issues.md +204 -49
- package/docs/troubleshooting.md +33 -1
- package/package.json +1 -1
- package/packages/adapters/src/claude-hooks.js +77 -0
- package/packages/adapters/src/claude-transcript.js +74 -0
- package/packages/adapters/src/codex-hooks.js +152 -0
- package/packages/adapters/src/codex-transcript.js +73 -0
- package/packages/adapters/test/claude-hooks.test.mjs +87 -0
- package/packages/adapters/test/claude-transcript.test.mjs +70 -0
- package/packages/adapters/test/codex-hooks.test.mjs +120 -0
- package/packages/adapters/test/codex-transcript.test.mjs +97 -0
- package/packages/app-state/src/state.js +21 -1
- package/packages/cli-core/src/claude-hook-injection.js +42 -0
- package/packages/cli-core/src/claude-transcript-watcher.js +185 -0
- package/packages/cli-core/src/codex-hook-injection.js +49 -0
- package/packages/cli-core/src/codex-transcript-watcher.js +160 -0
- package/packages/cli-core/src/run-command.js +7 -3
- package/packages/cli-core/src/run-state.js +87 -0
- package/packages/cli-core/test/claude-hook-injection.test.mjs +45 -0
- package/packages/cli-core/test/claude-transcript-watcher.test.mjs +121 -0
- package/packages/cli-core/test/codex-hook-injection.test.mjs +45 -0
- package/packages/cli-core/test/codex-transcript-watcher.test.mjs +108 -0
- package/packages/cli-core/test/run-command.test.mjs +20 -0
- package/packages/cli-core/test/run-state.test.mjs +113 -0
- package/packages/daemon-core/src/approval-process-watcher.js +169 -0
- package/packages/daemon-core/test/approval-process-watcher.test.mjs +295 -0
- package/packages/platform-core/src/process-snapshot.js +88 -0
- package/packages/platform-core/test/process-snapshot.test.mjs +105 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Haya Pet are documented here. This project adheres to
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
> Note: some entries originally drafted under 0.2.0 actually landed *after* the
|
|
7
|
+
> 0.2.0 npm publish; they are listed under 0.2.1, which is the first version that
|
|
8
|
+
> ships them.
|
|
9
|
+
|
|
10
|
+
## [0.2.1]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Approval-accept detection** — when you **approve** a permission prompt for a
|
|
14
|
+
command, the pet now flips from *waiting for approval* to *working* a couple of
|
|
15
|
+
seconds after the command actually starts, instead of showing "waiting" for the
|
|
16
|
+
tool's whole run. Clients emit **no event at the accept moment** (verified for
|
|
17
|
+
Claude Code: no hook, no transcript record; `PostToolUse` only fires when the
|
|
18
|
+
tool *finishes*) — so for a long approved build/test the pet used to sit on
|
|
19
|
+
"waiting" for minutes. Detection is **event-based, never a timer**: while a
|
|
20
|
+
session waits, the companion watches the client's **process tree**, and only a
|
|
21
|
+
new process that verifiably starts under the client (and survives two
|
|
22
|
+
consecutive polls, filtering hook blips) counts as an approval. An unanswered
|
|
23
|
+
prompt spawns nothing, so the warning stays up until you actually decide.
|
|
24
|
+
In-process approvals (file edits) aren't detected but complete in milliseconds
|
|
25
|
+
after approval anyway. Windows verified live; macOS (`ps`) and Linux (`/proc`)
|
|
26
|
+
listers are included pending live hardware verification. Details in
|
|
27
|
+
`docs/known-issues.md`.
|
|
28
|
+
- **`haya-pet hooks on` / `off` / `status`** — persists the live-status preference,
|
|
29
|
+
so you enable it once instead of setting an env var every shell. The toggle is
|
|
30
|
+
**global**: it covers every hook-capable client (Claude Code and Codex).
|
|
31
|
+
`HAYA_PET_HOOKS=1` (on) / `HAYA_PET_NO_HOOKS=1` (off) still work as per-run overrides.
|
|
32
|
+
- **Codex live status via per-session hooks** (opt-in: `haya-pet hooks on`). haya-pet
|
|
33
|
+
injects a stable `~/.codex/haya-pet.config.toml` profile and launches
|
|
34
|
+
`codex -p haya-pet`, layering the hooks on top of your base config (auth/model/MCP
|
|
35
|
+
untouched). The hooks report through the same `haya-pet state` reporter, with full
|
|
36
|
+
terminal fidelity. First run shows Codex's one-time *review hooks* prompt; approve
|
|
37
|
+
it once. If you already pass your own `-p/--profile`, haya-pet skips injection and
|
|
38
|
+
says so (Codex allows only one profile). Hooks cover `thinking` (turn start /
|
|
39
|
+
after tools) and `idle` (turn end); a **Codex transcript watcher** fills in tool
|
|
40
|
+
activity (`running_tool` / `editing_files`) by tailing the session JSONL, since
|
|
41
|
+
Codex's `PreToolUse` hook doesn't fire upstream yet
|
|
42
|
+
([openai/codex#16732](https://github.com/openai/codex/issues/16732)).
|
|
43
|
+
*Waiting for approval* stays unavailable for Codex until that lands.
|
|
44
|
+
- **L3 transcript watcher (Claude Code)** — tails Claude's session JSONL to reliably
|
|
45
|
+
clear *waiting for approval* when a permission is **denied** (Claude fires no hook
|
|
46
|
+
on a manual denial). Ground-truth based, never a timer, so a genuinely-pending
|
|
47
|
+
approval keeps alerting until you actually decide.
|
|
48
|
+
- **`PermissionRequest` hook** for a snappier *waiting for approval* cue (fires the
|
|
49
|
+
instant the dialog appears, ahead of the notification).
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- Pet stuck on *waiting for approval* after a manual **denial** (see the Claude
|
|
53
|
+
transcript watcher above).
|
|
54
|
+
- Pet stuck on *waiting for approval* after an **accept**, for as long as the
|
|
55
|
+
approved tool kept running (see approval-accept detection above).
|
|
56
|
+
- `Notification` events other than permission prompts (e.g. `idle_prompt`) were
|
|
57
|
+
mislabeled as *waiting for approval*; they are now mapped correctly.
|
|
58
|
+
|
|
59
|
+
## [0.2.0]
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
- **`haya-pet run` now defaults to native passthrough** (`stdio: "inherit"`). The
|
|
63
|
+
wrapped CLI talks directly to your terminal, so **Shift+Tab**, mouse-wheel
|
|
64
|
+
scroll, and word-edit all work normally. PTY observation is now opt-in via
|
|
65
|
+
`--observe` (it routes input through ConPTY on Windows, which can mangle special
|
|
66
|
+
keys — use it only for non-interactive runs).
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
- **Claude Code live status via per-session hooks** (opt-in: `HAYA_PET_HOOKS=1` in
|
|
70
|
+
this release; 0.2.1 adds the persisted `haya-pet hooks on` toggle).
|
|
71
|
+
Injects a stable settings file through `claude --settings <file>` — no change to
|
|
72
|
+
your global config — wiring Claude's events to a new `haya-pet state` reporter so
|
|
73
|
+
the pet shows thinking / running tools / editing files / waiting for approval,
|
|
74
|
+
with full terminal fidelity (no PTY). First run shows Claude's one-time
|
|
75
|
+
*review hooks* prompt; approve it once.
|
|
76
|
+
- **`haya-pet state <state>` command** — reporter used by client hooks to push live
|
|
77
|
+
status to the daemon over IPC.
|
|
78
|
+
- **`HAYA_PET_HOOK_DEBUG=<file>`** — append one JSONL line per status event
|
|
79
|
+
(hook- and transcript-sourced) for diagnostics.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
- Claude Code TUI accepted no keyboard input when hooks were injected — caused by a
|
|
83
|
+
volatile per-session argument and temp path that re-triggered Claude's hook-trust
|
|
84
|
+
review every launch. Hook commands and the settings path are now stable; the
|
|
85
|
+
session id is passed via the `HAYA_PET_SESSION_ID` env var.
|
|
86
|
+
|
|
87
|
+
### Notes
|
|
88
|
+
- In this release Codex and Antigravity had no hook adapter — native passthrough
|
|
89
|
+
with lifecycle status, or `--observe` for coarse PTY activity. 0.2.1 adds the
|
|
90
|
+
Codex adapter; Antigravity remains a planned follow-up.
|
|
91
|
+
|
|
92
|
+
## [0.1.0]
|
|
93
|
+
- Initial generic AI CLI pet runtime: overlay companion, session bubbles, daemon
|
|
94
|
+
IPC, client adapters, pet asset pipeline, cross-OS paths.
|
package/README.md
CHANGED
|
@@ -51,7 +51,9 @@ Haya Pet watches all of them and presents one ambient interface:
|
|
|
51
51
|
- 🧠 **Normalized state model** — every client maps to a shared state vocabulary
|
|
52
52
|
(`thinking`, `running_tool`, `waiting_approval`, `reviewing`, `failed`, …).
|
|
53
53
|
- 🧩 **Client adapters** with tiered support (process wrapper → PTY observer →
|
|
54
|
-
|
|
54
|
+
client hooks) so the daemon never bakes in client-specific logic. Default is
|
|
55
|
+
lifecycle status; richer status is opt-in (Claude Code / Codex hooks via
|
|
56
|
+
`haya-pet hooks on`, or PTY `--observe` for any client).
|
|
55
57
|
- 🚀 **Zero-setup launch** — `haya-pet run …` auto-starts the overlay; no separate
|
|
56
58
|
daemon to manage.
|
|
57
59
|
- 🖼️ **Codex-compatible pet assets** (1536×1872 sprite atlas, 9 actions).
|
|
@@ -92,8 +94,10 @@ Haya Pet watches all of them and presents one ambient interface:
|
|
|
92
94
|
| **Node ≥ 18** | Runtime + companion (Electron) |
|
|
93
95
|
| **npm** | Install + scripts |
|
|
94
96
|
|
|
95
|
-
>
|
|
96
|
-
>
|
|
97
|
+
> Default status is lifecycle-only and needs no extra modules. Opt-in Claude Code /
|
|
98
|
+
> Codex hooks (`haya-pet hooks on`) also need none. The opt-in `--observe` PTY mode uses
|
|
99
|
+
> `node-pty` (installed automatically when it can build; without it, `--observe`
|
|
100
|
+
> degrades to lifecycle-only tracking).
|
|
97
101
|
|
|
98
102
|
## Install
|
|
99
103
|
|
|
@@ -138,19 +142,57 @@ shows success (a green check) or failure (a red cross), then fades.
|
|
|
138
142
|
|
|
139
143
|
### Live activity status
|
|
140
144
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
`haya-pet run` uses **native passthrough by default** — the CLI talks directly to
|
|
146
|
+
your terminal, so every input mode (Shift+Tab, mouse wheel, word-edit) works
|
|
147
|
+
exactly as it does without the wrapper. Out of the box, every client shows
|
|
148
|
+
**lifecycle status** (a session bubble while it runs; success/failure from the
|
|
149
|
+
real exit code, never from scraping "error" out of output).
|
|
145
150
|
|
|
146
151
|
```bash
|
|
147
|
-
haya-pet run -- claude
|
|
148
|
-
haya-pet run --
|
|
152
|
+
haya-pet run --client claude-code -- claude # full fidelity, lifecycle status
|
|
153
|
+
haya-pet run --client codex -- codex # full fidelity, lifecycle status
|
|
149
154
|
```
|
|
150
155
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
Two **opt-in** ways to get richer *in-session* status (thinking / running tools /
|
|
157
|
+
editing files / waiting for approval):
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Claude Code AND Codex — live status via per-session hooks, NO terminal-fidelity
|
|
161
|
+
# tradeoff. Enable once (persisted, global); the first run for each client shows a
|
|
162
|
+
# one-time "review hooks" prompt you approve once.
|
|
163
|
+
haya-pet hooks on
|
|
164
|
+
haya-pet run --client claude-code -- claude
|
|
165
|
+
haya-pet run --client codex -- codex
|
|
166
|
+
# (per-run override without persisting: HAYA_PET_HOOKS=1 …, or $env:HAYA_PET_HOOKS=1 in PowerShell)
|
|
167
|
+
# (turn back off: haya-pet hooks off · check: haya-pet hooks status)
|
|
168
|
+
|
|
169
|
+
# Any client — coarse live status by watching output through a PTY.
|
|
170
|
+
haya-pet run --observe --client codex -- codex
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
> **Codex coverage.** Codex shows `thinking` (working) and `idle` (done) via hooks,
|
|
174
|
+
> plus `running_tool` / `editing_files` via a session-transcript watcher.
|
|
175
|
+
> *Waiting for approval* doesn't arrive yet because of an upstream gap where
|
|
176
|
+
> Codex's `PermissionRequest` hook doesn't fire
|
|
177
|
+
> ([openai/codex#16732](https://github.com/openai/codex/issues/16732)); it'll start
|
|
178
|
+
> working automatically once Codex fixes it. Also: if you pass your own
|
|
179
|
+
> `-p/--profile` to codex, haya-pet skips hook injection (Codex allows one
|
|
180
|
+
> profile) and tells you. Claude Code has full coverage.
|
|
181
|
+
|
|
182
|
+
> **Approval prompts resolve correctly** (Claude Code): deny → the pet returns to
|
|
183
|
+
> idle the moment the denial lands in the session transcript; accept a command →
|
|
184
|
+
> the pet flips to *working* a couple of seconds after the approved command
|
|
185
|
+
> actually starts running (detected from the client's process tree — a real
|
|
186
|
+
> event, never a timeout, so an unanswered prompt keeps warning until you decide).
|
|
187
|
+
|
|
188
|
+
> **Why opt-in?**
|
|
189
|
+
> - **Hooks (Claude Code / Codex):** injecting hooks makes the client show a
|
|
190
|
+
> one-time *review hooks* trust prompt. We don't disrupt your session by default;
|
|
191
|
+
> turn it on once with `haya-pet hooks on` when you're happy to approve the hooks.
|
|
192
|
+
> - **`--observe` (any client):** PTY observation infers status from output, but on
|
|
193
|
+
> Windows it routes input through ConPTY, which can break **Shift+Tab**, mouse
|
|
194
|
+
> scroll, and word-edit. Use it only for non-interactive runs. See
|
|
195
|
+
> [docs/known-issues.md](docs/known-issues.md).
|
|
154
196
|
|
|
155
197
|
## Add and choose a pet
|
|
156
198
|
|
|
@@ -219,14 +261,14 @@ Full list (incl. repairing a broken Electron install): [docs/troubleshooting.md]
|
|
|
219
261
|
|
|
220
262
|
| Client | Status | Support level |
|
|
221
263
|
|---|---|---|
|
|
222
|
-
| Generic CLI | ✅ | L1 process wrapper |
|
|
223
|
-
| Codex | ✅ | L1 +
|
|
224
|
-
| Claude Code | ✅ | L1 +
|
|
225
|
-
| Antigravity | ✅ | L1 wrapper |
|
|
264
|
+
| Generic CLI | ✅ | L1 process wrapper (+ L2 PTY via `--observe`) |
|
|
265
|
+
| Codex | ✅ | L1 wrapper + **L4 live-status hooks** (opt-in `haya-pet hooks on`; partial — see note) |
|
|
266
|
+
| Claude Code | ✅ | L1 wrapper + **L4 live-status hooks** (opt-in `haya-pet hooks on`) |
|
|
267
|
+
| Antigravity | ✅ | L1 wrapper (+ L2 PTY via `--observe`) |
|
|
226
268
|
| Gemini CLI / Aider / others | 🔜 | via the generic adapter |
|
|
227
269
|
|
|
228
270
|
(See [docs/architecture.md](docs/architecture.md) for the support tiers and the
|
|
229
|
-
platform matrix.)
|
|
271
|
+
platform matrix, and [CHANGELOG.md](CHANGELOG.md) for release notes.)
|
|
230
272
|
|
|
231
273
|
## Privacy
|
|
232
274
|
|
package/apps/cli/src/haya-pet.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { realpathSync, appendFileSync } from "node:fs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { runGenericCommand as defaultRunGenericCommand } from "../../../packages/cli-core/src/run-command.js";
|
|
7
|
+
import { parseStateArgs, runStateCommand } from "../../../packages/cli-core/src/run-state.js";
|
|
8
|
+
import { injectClaudeHooks as defaultInjectClaudeHooks } from "../../../packages/cli-core/src/claude-hook-injection.js";
|
|
9
|
+
import { injectCodexHooks as defaultInjectCodexHooks } from "../../../packages/cli-core/src/codex-hook-injection.js";
|
|
10
|
+
import { watchClaudeTranscript as defaultWatchClaudeTranscript } from "../../../packages/cli-core/src/claude-transcript-watcher.js";
|
|
11
|
+
import { watchCodexTranscript as defaultWatchCodexTranscript } from "../../../packages/cli-core/src/codex-transcript-watcher.js";
|
|
6
12
|
import { ensureCompanionConnection } from "../../../packages/cli-core/src/companion-launcher.js";
|
|
7
13
|
import { createIpcClient as defaultCreateIpcClient } from "../../../packages/daemon-core/src/ipc-server.js";
|
|
8
14
|
import { getDefaultPaths } from "../../../packages/platform-core/src/paths.js";
|
|
9
15
|
import { discoverPets as defaultDiscoverPets } from "../../../packages/pet-core/src/discovery.js";
|
|
10
16
|
import { createStateFile as defaultCreateStateFile } from "../../../packages/app-state/src/state-file.js";
|
|
11
|
-
import { getSelectedPetId, setSelectedPet } from "../../../packages/app-state/src/state.js";
|
|
17
|
+
import { getSelectedPetId, setSelectedPet, getHooksEnabled, setHooksEnabled } from "../../../packages/app-state/src/state.js";
|
|
12
18
|
import { getAdapterInfo } from "../../../packages/adapters/src/adapter-info.js";
|
|
13
19
|
|
|
14
20
|
const CLIENT_DISPLAY_NAMES = Object.freeze({
|
|
@@ -41,6 +47,14 @@ export function parseAiPetArgs(argv) {
|
|
|
41
47
|
return { command: "stop" };
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
if (command === "state") {
|
|
51
|
+
return parseStateArgs(rest);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (command === "hooks") {
|
|
55
|
+
return parseHooksArgs(rest);
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
throw new Error(`Unsupported haya-pet command: ${command}`);
|
|
45
59
|
}
|
|
46
60
|
|
|
@@ -59,6 +73,14 @@ export async function runAiPet(argv, dependencies = {}) {
|
|
|
59
73
|
return runStopCommand(parsed, dependencies);
|
|
60
74
|
}
|
|
61
75
|
|
|
76
|
+
if (parsed.command === "state") {
|
|
77
|
+
return runStateCommand(parsed, dependencies);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (parsed.command === "hooks") {
|
|
81
|
+
return runHooksCommand(parsed, dependencies);
|
|
82
|
+
}
|
|
83
|
+
|
|
62
84
|
return runRunCommand(parsed, dependencies);
|
|
63
85
|
}
|
|
64
86
|
|
|
@@ -102,26 +124,213 @@ export async function runStartCommand(_parsed, dependencies = {}) {
|
|
|
102
124
|
|
|
103
125
|
async function runRunCommand(parsed, dependencies) {
|
|
104
126
|
const runGenericCommand = dependencies.runGenericCommand ?? defaultRunGenericCommand;
|
|
127
|
+
const injectClaudeHooks = dependencies.injectClaudeHooks ?? defaultInjectClaudeHooks;
|
|
128
|
+
const injectCodexHooks = dependencies.injectCodexHooks ?? defaultInjectCodexHooks;
|
|
129
|
+
const watchClaudeTranscript = dependencies.watchClaudeTranscript ?? defaultWatchClaudeTranscript;
|
|
130
|
+
const watchCodexTranscript = dependencies.watchCodexTranscript ?? defaultWatchCodexTranscript;
|
|
131
|
+
const print = dependencies.print ?? defaultPrint;
|
|
132
|
+
const env = dependencies.env ?? process.env;
|
|
133
|
+
const now = dependencies.now ?? Date.now;
|
|
134
|
+
const cwd = dependencies.cwd ?? process.cwd();
|
|
105
135
|
const messageSender = await createMessageSender(dependencies);
|
|
106
136
|
|
|
137
|
+
const sessionId = dependencies.sessionId ?? `sess_${randomUUID()}`;
|
|
138
|
+
let childArgs = parsed.childArgs;
|
|
139
|
+
let childEnv = env;
|
|
140
|
+
let cleanup = () => {};
|
|
141
|
+
let stopWatcher = () => {};
|
|
142
|
+
|
|
143
|
+
// Native passthrough is always the default (full terminal fidelity). Live-status
|
|
144
|
+
// hooks are OPT-IN — persisted via `haya-pet hooks on`, or per-run via
|
|
145
|
+
// HAYA_PET_HOOKS=1 — because injecting hooks makes the client show a one-time
|
|
146
|
+
// "review hooks" trust prompt; we never disrupt the user's session uninvited.
|
|
147
|
+
// Both clients report live status via the `haya-pet state` reporter (no PTY, so
|
|
148
|
+
// Shift+Tab works); the session id rides in via HAYA_PET_SESSION_ID.
|
|
149
|
+
const hooksOn = await resolveHooksEnabled(env, dependencies);
|
|
150
|
+
|
|
151
|
+
// Claude Code: inject a stable `--settings` file.
|
|
152
|
+
const claudeHooksOn = hooksOn && parsed.clientId === "claude-code";
|
|
153
|
+
if (claudeHooksOn) {
|
|
154
|
+
const injected = injectClaudeHooks();
|
|
155
|
+
childArgs = [...parsed.childArgs, "--settings", injected.settingsPath];
|
|
156
|
+
childEnv = { ...env, HAYA_PET_SESSION_ID: sessionId };
|
|
157
|
+
cleanup = injected.cleanup;
|
|
158
|
+
|
|
159
|
+
// Claude fires NO hook when the user manually denies a permission, so the
|
|
160
|
+
// pet would stay stuck on "waiting for approval". Tail the session transcript
|
|
161
|
+
// (ground truth) and clear to idle the moment a denial is recorded — never on
|
|
162
|
+
// a timer, so a genuinely-pending approval keeps alerting until it's resolved.
|
|
163
|
+
const watcher = watchClaudeTranscript({
|
|
164
|
+
cwd,
|
|
165
|
+
homeDir: dependencies.homeDir,
|
|
166
|
+
startedAt: now(),
|
|
167
|
+
onDenial: (event) => {
|
|
168
|
+
hookDebugLog(env, now, { source: "transcript", event: "denied", state: "idle", toolUseId: event?.toolUseId });
|
|
169
|
+
messageSender
|
|
170
|
+
.send({
|
|
171
|
+
type: "state",
|
|
172
|
+
sessionId,
|
|
173
|
+
state: "idle",
|
|
174
|
+
summary: "approval denied",
|
|
175
|
+
confidence: 0.9,
|
|
176
|
+
source: "client_log",
|
|
177
|
+
updatedAt: now()
|
|
178
|
+
})
|
|
179
|
+
.catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
stopWatcher = watcher.stop;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Codex: no `--settings` equivalent, so inject a stable profile and add
|
|
186
|
+
// `-p <name>` at the FRONT (a global flag must precede any subcommand). Codex
|
|
187
|
+
// takes only one profile, so if the user already passes their own -p/--profile
|
|
188
|
+
// we skip injection and say so rather than clobber their choice. Codex
|
|
189
|
+
// PreToolUse is not reliable, so a transcript watcher supplies tool activity.
|
|
190
|
+
const codexHooksOn = hooksOn && parsed.clientId === "codex";
|
|
191
|
+
if (codexHooksOn) {
|
|
192
|
+
if (hasProfileArg(parsed.childArgs)) {
|
|
193
|
+
print(
|
|
194
|
+
"haya-pet: Codex live-status hooks skipped — you passed your own -p/--profile (Codex allows only one)."
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
const injected = injectCodexHooks();
|
|
198
|
+
childArgs = ["-p", injected.profileName, ...parsed.childArgs];
|
|
199
|
+
childEnv = { ...env, HAYA_PET_SESSION_ID: sessionId };
|
|
200
|
+
cleanup = injected.cleanup;
|
|
201
|
+
|
|
202
|
+
const activeToolCalls = new Set();
|
|
203
|
+
const watcher = watchCodexTranscript({
|
|
204
|
+
homeDir: dependencies.homeDir,
|
|
205
|
+
sessionsRoot: dependencies.codexSessionsRoot,
|
|
206
|
+
startedAt: now(),
|
|
207
|
+
onToolEvent: (event) => {
|
|
208
|
+
hookDebugLog(env, now, {
|
|
209
|
+
source: "codex_transcript",
|
|
210
|
+
event: event.type,
|
|
211
|
+
toolCallId: event.toolCallId,
|
|
212
|
+
toolName: event.toolName,
|
|
213
|
+
state: event.state
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (event.type === "tool_started") {
|
|
217
|
+
activeToolCalls.add(event.toolCallId);
|
|
218
|
+
messageSender
|
|
219
|
+
.send({
|
|
220
|
+
type: "state",
|
|
221
|
+
sessionId,
|
|
222
|
+
state: event.state,
|
|
223
|
+
summary: event.toolName,
|
|
224
|
+
confidence: 0.85,
|
|
225
|
+
source: "client_log",
|
|
226
|
+
updatedAt: now()
|
|
227
|
+
})
|
|
228
|
+
.catch(() => {});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (event.type === "tool_finished") {
|
|
233
|
+
activeToolCalls.delete(event.toolCallId);
|
|
234
|
+
if (activeToolCalls.size === 0) {
|
|
235
|
+
messageSender
|
|
236
|
+
.send({
|
|
237
|
+
type: "state",
|
|
238
|
+
sessionId,
|
|
239
|
+
state: "thinking",
|
|
240
|
+
confidence: 0.85,
|
|
241
|
+
source: "client_log",
|
|
242
|
+
updatedAt: now()
|
|
243
|
+
})
|
|
244
|
+
.catch(() => {});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const previousStopWatcher = stopWatcher;
|
|
250
|
+
stopWatcher = () => {
|
|
251
|
+
watcher.stop();
|
|
252
|
+
previousStopWatcher();
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
107
257
|
try {
|
|
108
258
|
return await runGenericCommand({
|
|
109
259
|
command: parsed.childCommand,
|
|
110
|
-
args:
|
|
111
|
-
cwd
|
|
260
|
+
args: childArgs,
|
|
261
|
+
cwd,
|
|
112
262
|
clientId: parsed.clientId,
|
|
113
263
|
clientDisplayName: CLIENT_DISPLAY_NAMES[parsed.clientId] ?? parsed.clientId,
|
|
114
264
|
observe: parsed.observe,
|
|
265
|
+
sessionId,
|
|
266
|
+
env: childEnv,
|
|
115
267
|
heartbeatIntervalMs: dependencies.heartbeatIntervalMs,
|
|
116
268
|
now: dependencies.now,
|
|
117
269
|
stdio: dependencies.stdio,
|
|
118
270
|
send: messageSender.send
|
|
119
271
|
});
|
|
120
272
|
} finally {
|
|
273
|
+
stopWatcher();
|
|
274
|
+
cleanup();
|
|
121
275
|
await messageSender.close();
|
|
122
276
|
}
|
|
123
277
|
}
|
|
124
278
|
|
|
279
|
+
// Resolve whether live-status hooks should be injected for this run (any
|
|
280
|
+
// hook-capable client). Precedence: HAYA_PET_NO_HOOKS forces off, HAYA_PET_HOOKS
|
|
281
|
+
// forces on (per-run overrides), otherwise the persisted `haya-pet hooks on/off`
|
|
282
|
+
// preference.
|
|
283
|
+
async function resolveHooksEnabled(env, dependencies) {
|
|
284
|
+
if (isTruthyFlag(env.HAYA_PET_NO_HOOKS)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
if (isTruthyFlag(env.HAYA_PET_HOOKS)) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const state = await createConfigStateFile(dependencies).load();
|
|
292
|
+
return getHooksEnabled(state);
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isTruthyFlag(value) {
|
|
299
|
+
return value === "1" || value === "true";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Detect a user-supplied Codex profile flag so we don't clobber it: -p, --profile,
|
|
303
|
+
// or the `--profile=foo` / `-p=foo` forms.
|
|
304
|
+
function hasProfileArg(args) {
|
|
305
|
+
return args.some(
|
|
306
|
+
(arg) => arg === "-p" || arg === "--profile" || arg.startsWith("--profile=") || arg.startsWith("-p=")
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function createConfigStateFile(dependencies) {
|
|
311
|
+
const paths = getDefaultPaths({
|
|
312
|
+
platform: dependencies.platform,
|
|
313
|
+
env: dependencies.env,
|
|
314
|
+
homeDir: dependencies.homeDir
|
|
315
|
+
});
|
|
316
|
+
const createStateFile = dependencies.createStateFile ?? defaultCreateStateFile;
|
|
317
|
+
return createStateFile({ statePath: paths.statePath });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Best-effort: mirror the reporter's HAYA_PET_HOOK_DEBUG log so transcript-driven
|
|
321
|
+
// events (which don't go through `haya-pet state`) show up in the same trace.
|
|
322
|
+
function hookDebugLog(env, now, entry) {
|
|
323
|
+
const target = env.HAYA_PET_HOOK_DEBUG;
|
|
324
|
+
if (!target) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
appendFileSync(target, `${JSON.stringify({ ts: now(), ...entry })}\n`);
|
|
329
|
+
} catch {
|
|
330
|
+
// diagnostics must never break the run
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
125
334
|
export async function runPetsCommand(parsed, dependencies = {}) {
|
|
126
335
|
const paths = getDefaultPaths({
|
|
127
336
|
platform: dependencies.platform,
|
|
@@ -181,6 +390,38 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
181
390
|
return result;
|
|
182
391
|
}
|
|
183
392
|
|
|
393
|
+
function parseHooksArgs(args) {
|
|
394
|
+
const [action = "status"] = args;
|
|
395
|
+
if (action === "on" || action === "off" || action === "status") {
|
|
396
|
+
return { command: "hooks", action };
|
|
397
|
+
}
|
|
398
|
+
throw new Error(`Unknown hooks action: ${action} (use on, off, or status)`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Persisted GLOBAL toggle for live-status hooks (the convenient alternative to
|
|
402
|
+
// setting HAYA_PET_HOOKS every shell). Covers every hook-capable client — Claude
|
|
403
|
+
// Code and Codex today.
|
|
404
|
+
export async function runHooksCommand(parsed, dependencies = {}) {
|
|
405
|
+
const print = dependencies.print ?? defaultPrint;
|
|
406
|
+
const stateFile = createConfigStateFile(dependencies);
|
|
407
|
+
const state = await stateFile.load();
|
|
408
|
+
|
|
409
|
+
if (parsed.action === "status") {
|
|
410
|
+
const enabled = getHooksEnabled(state);
|
|
411
|
+
print(`Live-status hooks: ${enabled ? "on" : "off"}`);
|
|
412
|
+
return { command: "hooks", action: "status", enabled };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const enabled = parsed.action === "on";
|
|
416
|
+
await stateFile.save(setHooksEnabled(state, enabled));
|
|
417
|
+
print(
|
|
418
|
+
enabled
|
|
419
|
+
? "Live-status hooks: on. The first `haya-pet run` for Claude Code or Codex asks the client to review the hooks once — approve it."
|
|
420
|
+
: "Live-status hooks: off."
|
|
421
|
+
);
|
|
422
|
+
return { command: "hooks", action: parsed.action, enabled };
|
|
423
|
+
}
|
|
424
|
+
|
|
184
425
|
function parsePetsArgs(args) {
|
|
185
426
|
if (args.length === 0) {
|
|
186
427
|
return { command: "pets", action: "list" };
|
|
@@ -205,7 +446,7 @@ function parsePetsArgs(args) {
|
|
|
205
446
|
|
|
206
447
|
function parseRunArgs(args) {
|
|
207
448
|
let clientId = "generic";
|
|
208
|
-
let observe =
|
|
449
|
+
let observe = false; // native passthrough by default (full terminal fidelity); --observe opts in
|
|
209
450
|
let childStart = -1;
|
|
210
451
|
|
|
211
452
|
for (let index = 0; index < args.length; index += 1) {
|