eagle-mem 4.15.1 → 4.16.0
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 +13 -0
- package/README.md +16 -14
- package/docs/agent-compatibility/README.md +1 -0
- package/docs/agent-compatibility/factory.md +56 -0
- package/hooks/post-tool-use.sh +9 -4
- package/hooks/pre-compact.sh +5 -4
- package/hooks/pre-tool-use.sh +3 -3
- package/hooks/stop.sh +4 -4
- package/lib/common.sh +17 -1
- package/lib/factory-hooks.sh +183 -0
- package/lib/hooks-posttool.sh +2 -2
- package/package.json +3 -2
- package/scripts/doctor.sh +13 -1
- package/scripts/install.sh +28 -3
- package/scripts/test.sh +1 -0
- package/scripts/uninstall.sh +12 -0
- package/scripts/update.sh +15 -1
- package/tests/fixtures/agent-hooks/factory-create.json +10 -0
- package/tests/fixtures/agent-hooks/factory-post-tool-use.json +12 -0
- package/tests/test_agent_compatibility_docs_gate.sh +18 -1
- package/tests/test_factory_hooks_config.sh +132 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to the **Eagle Mem** project are documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## v4.16.0 Factory Droid as a First-Class Agent
|
|
8
|
+
|
|
9
|
+
Factory Droid (the `droid` CLI) joins Claude Code, Codex, OpenCode, and Antigravity as a fully hook-backed agent. It now gets the complete automatic lifecycle — SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, and PreCompact — with no manual setup beyond installing Eagle Mem.
|
|
10
|
+
|
|
11
|
+
- **Why.** Factory Droid was previously unsupported. A hand-rolled scaffold (a `~/.factory/hooks.json` plus a translation adapter) had been attempted but was silently dead: Factory does not read `hooks.json` — its hooks live in `~/.factory/settings.json`, the **same** file that holds the user's `apiKey`, `customModels`, and `statusLine`. Registering there demands the same secret-preserving discipline Eagle Mem already uses for Claude/Codex/OpenCode config files.
|
|
12
|
+
- **What.** New `lib/factory-hooks.sh` registers the 8 handlers into `~/.factory/settings.json` using the documented Factory entry schema `{ "type": "command", "command": "<shell>", "timeout": <seconds> }` — no `statusMessage` key (it isn't in the schema, and an unknown key in an `apiKey`-bearing file risks strict-parse breakage). Handlers are grouped under `hooks.<Event>` as matcher-groups; matchers (`PreToolUse`/`PostToolUse`/`PreCompact` only) use Factory's tool names — `Create` (write), `Execute` (shell), `Edit`, `Read`. There is no translation adapter: each handler sets `EAGLE_AGENT_SOURCE=factory` and invokes the shared `$EAGLE_MEM_DIR/hooks/<hook>.sh` directly, so rows record `agent = factory` (displayed as `Factory Droid`). Factory's transcript is a third JSONL schema (`type:"message"`, role at `.message.role`, `.message.content` always an array); the existing `or`-union jq selectors already serve it, so capture works with no extraction rewrite.
|
|
13
|
+
- **Safety.** Registration rewrites **only** the `.hooks` subtree of `settings.json`. `apiKey`, `customModels`, `statusLine`, and any other sibling are preserved verbatim, written atomically (tmpfile + `mv`) with a once-per-run backup before the first write, and the file's `600` mode is inherited. The event name is indexed dynamically via a jq `--arg` (injection-safe). Removal (`eagle_remove_factory_hooks`, called by `uninstall.sh`) strips only commands containing `.eagle-mem/hooks/` and leaves secrets intact. No change to any shared hook's stdin/stdout/exit contract.
|
|
14
|
+
- **Registration.** `scripts/update.sh` registers the Factory hooks when Factory is detected (`~/.factory` exists or `droid` is on `PATH`) and `jq` is present; `eagle-mem update` adds them to existing installs automatically. `scripts/doctor.sh` reports Factory status. Registration deduplicates on `(matcher, command)` and updates a stale command in place at the same `.eagle-mem/hooks/<script>.sh` path (e.g. a wrapper-prefix change), so re-running is idempotent.
|
|
15
|
+
|
|
16
|
+
New coverage: `tests/test_factory_hooks_config.sh` (secret-preservation, exactly-8 `{type,command,timeout}` handlers, no `statusMessage`, correct matchers, idempotency, in-place update, clean removal) and fixtures `tests/fixtures/agent-hooks/factory-post-tool-use.json` + `factory-create.json`. Compatibility evidence: `docs/agent-compatibility/factory.md`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
7
20
|
## v4.15.1 Documentation & npm Listing Refresh
|
|
8
21
|
|
|
9
22
|
Docs-only patch. No runtime code change — no edits to hooks, `lib`, `db`, `scripts`, or any stdin/stdout/exit contract.
|
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
# Eagle Mem
|
|
9
9
|
|
|
10
|
-
**Shared memory, release guardrails, and worker lanes for Claude Code, Codex, OpenCode, Grok, and
|
|
10
|
+
**Shared memory, release guardrails, and worker lanes for Claude Code, Codex, OpenCode, Grok, Google Antigravity, and Factory Droid.**
|
|
11
11
|
|
|
12
|
-
Eagle Mem turns AI coding sessions into compounding project knowledge. It gives Claude Code, Codex, OpenCode,
|
|
12
|
+
Eagle Mem turns AI coding sessions into compounding project knowledge. It gives Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid hook-backed shared memory, gives Grok the same skills and CLI memory surface, labels which agent created each memory, blocks risky release commands until affected features are verified, and lets broad work split into durable worker lanes.
|
|
13
13
|
|
|
14
14
|
**v4.10.5 and onward focuses on Graph Memory, Dream Cycle Curation, OpenCode, Grok, Google Antigravity support, and Compaction Survival:** OpenCode users get a global local plugin plus linked skills, Grok users get first-class skill linking and `eagle-mem grok-bootstrap`, and Antigravity users get native Python SDK hook integration via `google_antigravity_hook.py`. Claude Code, Codex, OpenCode, and Antigravity receive the deepest automatic lifecycle support through hooks or plugins; Grok currently uses the shared CLI and skill workflow.
|
|
15
15
|
|
|
@@ -17,6 +17,8 @@ Eagle Mem turns AI coding sessions into compounding project knowledge. It gives
|
|
|
17
17
|
|
|
18
18
|
**v4.15.0 adds PreCompact Synchronous Capture:** a new lifecycle hook captures a rich, LLM-enriched session summary *before* Claude Code compacts the context window (manual or auto), closing the gap where the background enricher could otherwise finish after the window collapsed. It is side-effect-only — it never blocks compaction and never overwrites an agent-authored `eagle-mem session save`. Recent releases also added release-gate parity at the git layer (`eagle-mem gate` plus an opt-in `pre-push` hook) and provenance with a trust gate for curator-generated command rules (v4.14.0), plus a `busy_timeout` echo fix that closed a session→project mis-filing vector (v4.14.1). See [`CHANGELOG.md`](CHANGELOG.md).
|
|
19
19
|
|
|
20
|
+
**v4.16.0 adds Factory Droid as a first-class agent:** Factory Droid (the `droid` CLI) now gets the full automatic hook lifecycle — SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, and PreCompact — registered into `~/.factory/settings.json` alongside Claude Code, Codex, OpenCode, and Antigravity. Registration rewrites only the `.hooks` subtree and preserves the `apiKey`, `customModels`, and `statusLine` siblings in that file verbatim. Captured rows are source-attributed as `Factory Droid`, and Factory's distinct tool names (`Create`, `Execute`, `Edit`, `Read`) and transcript schema are handled natively. See [`CHANGELOG.md`](CHANGELOG.md).
|
|
21
|
+
|
|
20
22
|
**Website:** [Product](https://eagleisbatman.github.io/eagle-mem/) |
|
|
21
23
|
[Architecture](https://eagleisbatman.github.io/eagle-mem/architecture.html) |
|
|
22
24
|
[About](https://eagleisbatman.github.io/eagle-mem/about.html)
|
|
@@ -26,12 +28,12 @@ Eagle Mem turns AI coding sessions into compounding project knowledge. It gives
|
|
|
26
28
|
- **Start warmer** - every new session can recall project overviews, decisions, gotchas, summaries, hot files, mirrored memories, plans, and tasks.
|
|
27
29
|
- **Ship safer** - feature-mapped changes create pending verification records, and release-boundary commands stay blocked until the current diff is verified or waived.
|
|
28
30
|
- **Waste fewer tokens** - Eagle Mem injects compact context, nudges duplicate reads, and can route noisy shell output through RTK.
|
|
29
|
-
- **Coordinate agents** - Claude, Codex, OpenCode, Grok, and
|
|
31
|
+
- **Coordinate agents** - Claude, Codex, OpenCode, Grok, Antigravity, and Factory Droid can share one project memory while worker lanes record owner, model, effort, worktree, logs, validation, and handoff.
|
|
30
32
|
- **Stay local** - no daemon, no hosted memory service, no vector database. The core is hooks plus SQLite/FTS5.
|
|
31
33
|
|
|
32
34
|
## The Problem
|
|
33
35
|
|
|
34
|
-
Claude Code, Codex, OpenCode, Grok,
|
|
36
|
+
Claude Code, Codex, OpenCode, Grok, Google Antigravity, and Factory Droid start every session with amnesia. They don't remember what you built yesterday, what decisions you made, what files matter, or what broke last time. Every `/compact` wipes context. Every new session is a cold start. You waste tokens re-explaining your project, re-reading files, and watching agents repeat mistakes you already corrected.
|
|
35
37
|
|
|
36
38
|
The longer you work with Claude Code, the worse this gets. Projects accumulate history — decisions, gotchas, architectural patterns, feature dependencies — and none of it survives across sessions.
|
|
37
39
|
|
|
@@ -45,7 +47,7 @@ Eagle Mem is a local runtime layer for AI coding agents. It adds three things th
|
|
|
45
47
|
| **Guardrails** | "The agent cannot casually undo known decisions or push unverified feature changes." | Surfaces decisions before edits and enforces feature verification on push, PR, and publish boundaries. |
|
|
46
48
|
| **Lanes** | "A big task can survive compaction and split across agents." | Persists orchestrations, worker lanes, worktrees, logs, validation commands, and handoffs. |
|
|
47
49
|
|
|
48
|
-
Claude Code, Codex, OpenCode,
|
|
50
|
+
Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid share the same SQLite database at `~/.eagle-mem/memory.db`, and captured rows are source-attributed as `Claude Code`, `Codex`, `OpenCode`, `Antigravity`, or `Factory Droid`. Grok uses the same database through skills and CLI commands.
|
|
49
51
|
|
|
50
52
|
**Zero per-instance overhead.** No daemon, no vector DB, no MCP server. Just bash/python hooks, sqlite3 (WAL mode, FTS5 full-text search), and jq.
|
|
51
53
|
|
|
@@ -70,17 +72,17 @@ eagle-mem install
|
|
|
70
72
|
eagle-mem doctor
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
That's it. `doctor` should report a healthy install. Open Claude Code, Codex, or
|
|
75
|
+
That's it. `doctor` should report a healthy install. Open Claude Code, Codex, OpenCode, or Factory Droid, or import the Antigravity hook in your agent config, and Eagle Mem activates automatically. For Grok, run `eagle-mem grok-bootstrap` after install to confirm the linked skills and CLI workflow.
|
|
74
76
|
|
|
75
|
-
For Claude Code, Codex, OpenCode,
|
|
77
|
+
For Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid, everything is automatic. Eagle Mem scans your codebase, indexes source files, captures session summaries, mirrors memories and tasks (including planning-mode artifacts like `implementation_plan.md`, `task.md`, and `walkthrough.md`), learns which commands are noisy, and prunes stale data — all in the background via hooks or plugins.
|
|
76
78
|
|
|
77
|
-
For Google Antigravity, the installer copies the native Python integration to `~/.eagle-mem/integrations/google_antigravity_hook.py`. For OpenCode, it installs a global local plugin at `~/.config/opencode/plugins/eagle-mem.js` and symlinks skills into `~/.config/opencode/skills`. For Codex, the installer enables `hooks` in `~/.codex/config.toml`, registers hooks in `~/.codex/hooks.json`, symlinks Eagle Mem skills into `~/.codex/skills`, and patches `~/.codex/AGENTS.md`. For Claude Code, it integrates with `~/.claude/settings.json`, `CLAUDE.md`, and `~/.claude/skills`. Grok users get skill symlinks into `~/.grok/skills/` and can run `eagle-mem grok-bootstrap` for setup guidance and self-linking.
|
|
79
|
+
For Google Antigravity, the installer copies the native Python integration to `~/.eagle-mem/integrations/google_antigravity_hook.py`. For OpenCode, it installs a global local plugin at `~/.config/opencode/plugins/eagle-mem.js` and symlinks skills into `~/.config/opencode/skills`. For Codex, the installer enables `hooks` in `~/.codex/config.toml`, registers hooks in `~/.codex/hooks.json`, symlinks Eagle Mem skills into `~/.codex/skills`, and patches `~/.codex/AGENTS.md`. For Claude Code, it integrates with `~/.claude/settings.json`, `CLAUDE.md`, and `~/.claude/skills`. For Factory Droid, it registers the hook lifecycle into `~/.factory/settings.json`, preserving the `apiKey`, `customModels`, and `statusLine` already in that file. Grok users get skill symlinks into `~/.grok/skills/` and can run `eagle-mem grok-bootstrap` for setup guidance and self-linking.
|
|
78
80
|
|
|
79
81
|
### Prerequisites
|
|
80
82
|
|
|
81
83
|
- `sqlite3` with FTS5 support (ships with macOS; Eagle Mem prefers known system/Homebrew SQLite binaries before PATH shims)
|
|
82
84
|
- `jq` (the installer offers to install if missing)
|
|
83
|
-
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Codex, OpenCode, Google Antigravity SDK, or a Grok environment (`~/.grok/`)
|
|
85
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Codex, OpenCode, Google Antigravity SDK, Factory Droid (`~/.factory/`), or a Grok environment (`~/.grok/`)
|
|
84
86
|
|
|
85
87
|
## How It Works
|
|
86
88
|
|
|
@@ -114,7 +116,7 @@ Eagle Mem actively reduces token consumption:
|
|
|
114
116
|
|
|
115
117
|
### Compaction Survival
|
|
116
118
|
|
|
117
|
-
One of the core promises of Eagle Mem is protecting against `/compact` and session amnesia. Compaction Survival is project-level: it reads shared Eagle Mem state such as durable tasks, enriched summaries, stale work, and orchestration lanes. Claude Code, Codex, OpenCode,
|
|
119
|
+
One of the core promises of Eagle Mem is protecting against `/compact` and session amnesia. Compaction Survival is project-level: it reads shared Eagle Mem state such as durable tasks, enriched summaries, stale work, and orchestration lanes. Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid get automatic hook-backed or plugin-backed recovery through session start, turn-end, and compaction events. Grok can inspect and use the same state through `eagle-mem compaction`, `eagle-mem tasks`, and the linked skills.
|
|
118
120
|
|
|
119
121
|
Run `eagle-mem compaction` anytime to check readiness.
|
|
120
122
|
|
|
@@ -203,7 +205,7 @@ eagle-mem uninstall --dry-run
|
|
|
203
205
|
|
|
204
206
|
Install and update print the files/configs they intend to touch before they change the runtime. The installed runtime writes `~/.eagle-mem/install-manifest.json` with file sizes, modes, and SHA-256 hashes, so `doctor` can tell whether hooks, scripts, libraries, and database helpers still match the package that installed them.
|
|
205
207
|
|
|
206
|
-
Uninstall removes Claude Code, Codex, OpenCode, Grok,
|
|
208
|
+
Uninstall removes Claude Code, Codex, OpenCode, Grok, Google Antigravity, and Factory Droid hook/plugin registrations, Eagle Mem instruction blocks, custom Claude statusline integration, and skill links. It backs up user config files before editing them and keeps `~/.eagle-mem/memory.db` unless you explicitly confirm data deletion.
|
|
207
209
|
|
|
208
210
|
### Download Counts, Privacy, and Telemetry
|
|
209
211
|
|
|
@@ -290,12 +292,12 @@ Each lane is stored in `orchestration_lanes` and mirrored into `agent_tasks`, so
|
|
|
290
292
|
|
|
291
293
|
Both agents write to `~/.eagle-mem/memory.db`:
|
|
292
294
|
|
|
293
|
-
- `sessions.agent` records whether a session came from Claude Code, Codex, OpenCode, or
|
|
295
|
+
- `sessions.agent` records whether a session came from Claude Code, Codex, OpenCode, Antigravity, or Factory Droid
|
|
294
296
|
- `summaries.agent` records which agent produced the session summary
|
|
295
297
|
- mirrored memories, plans, and tasks include `origin_agent`
|
|
296
|
-
- SessionStart recall labels sources as `Claude Code`, `Codex`, `OpenCode`, or `
|
|
298
|
+
- SessionStart recall labels sources as `Claude Code`, `Codex`, `OpenCode`, `Antigravity`, or `Factory Droid`
|
|
297
299
|
|
|
298
|
-
That means opening the same project in Claude Code, Codex, OpenCode,
|
|
300
|
+
That means opening the same project in Claude Code, Codex, OpenCode, Google Antigravity, and Factory Droid does not create isolated memory worlds. They recall the same project history while preserving the source of each memory. Grok can search, inspect, and update that same project memory through the linked skills and CLI commands.
|
|
299
301
|
|
|
300
302
|
## Skills (Inside Claude Code, Codex, OpenCode, and Grok)
|
|
301
303
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Factory Droid Compatibility
|
|
2
|
+
|
|
3
|
+
Last verified: 2026-06-20
|
|
4
|
+
|
|
5
|
+
## Official Sources
|
|
6
|
+
|
|
7
|
+
- Factory documentation home: https://docs.factory.ai/
|
|
8
|
+
- Hooks guide (source): https://github.com/factory-ai/factory/blob/main/docs/cli/configuration/hooks-guide.mdx
|
|
9
|
+
- Hooks code-validation guide (source): https://github.com/factory-ai/factory/blob/main/docs/guides/hooks/code-validation.mdx
|
|
10
|
+
- Hooks auto-formatting guide (source): https://github.com/factory-ai/factory/blob/main/docs/guides/hooks/auto-formatting.mdx
|
|
11
|
+
|
|
12
|
+
## Behavior Relied On
|
|
13
|
+
|
|
14
|
+
- Factory Droid hooks live in `~/.factory/settings.json` — the **same** file that holds `apiKey`, `customModels`, and `statusLine`. Factory does not read a separate `hooks.json`; registration that targets such a file is silently dead. Eagle Mem rewrites only the `.hooks` subtree of `settings.json` and preserves every secret-bearing sibling verbatim.
|
|
15
|
+
- A hook entry is exactly `{ "type": "command", "command": "<shell>", "timeout": <seconds> }`. `type` (always `"command"`) and `command` are required; `timeout` is an optional integer measured in seconds. The schema has **no `statusMessage` field** — emitting an unknown key into this `apiKey`-bearing file risks strict-parse breakage, so Eagle Mem never writes one.
|
|
16
|
+
- Events are grouped under `hooks.<Event>` as an array of matcher-groups, each `{ "matcher": "<pattern>", "hooks": [ {type,command,timeout}, ... ] }`. The matcher-group container mirrors Claude Code's settings.json shape.
|
|
17
|
+
- `matcher` is optional and **case-sensitive**, and applies only to `PreToolUse`, `PostToolUse`, and `PreCompact`. `SessionStart` carries no matcher.
|
|
18
|
+
- Factory tool names differ from Claude: `Create` is the file-write tool (Claude `Write`), `Execute` is the shell tool (Claude `Bash`), `Edit` edits files, and `Read` reads them. The `Execute` tool input is at `.tool_input.command`; `Create` and `Edit` expose the path at `.tool_input.file_path`. Eagle Mem's write-detection (`lib/hooks-posttool.sh`) therefore treats `Create` as a write alongside `Write`/`Edit`/`apply_patch`.
|
|
19
|
+
- `PreCompact` registers both the `manual` and `auto` matchers (mirroring Claude) so rich pre-compaction capture fires on either trigger.
|
|
20
|
+
- The Factory transcript is a third schema: a JSONL stream where every line is `type:"message"`, the role is at `.message.role`, and `.message.content` is **always an array** of blocks (`{type:"text",...}` or `{type:"tool_use", name, input}`). Eagle Mem's `or`-union jq selectors let one extraction body serve both Claude and Factory transcripts.
|
|
21
|
+
- Factory exposes `$DROID_PROJECT_DIR` to hook commands. Eagle Mem does not depend on a translation adapter: it sets `EAGLE_AGENT_SOURCE=factory` and invokes the shared hook scripts in `$EAGLE_MEM_DIR/hooks/` directly, so the Factory lane records with `agent = factory` provenance across observations and summaries.
|
|
22
|
+
|
|
23
|
+
## Session Capture Behavior
|
|
24
|
+
|
|
25
|
+
- Factory replies stay prose-only; Eagle Mem never asks Factory to print summary blocks, XML, or hook payloads. The `Stop` hook captures a summary from the Factory transcript automatically (the third schema parses via the union selectors above).
|
|
26
|
+
- For a richer structured capture, Factory may run `eagle-mem session save --session-id <session_id> --agent factory ...` once at wrap-up. This sets `capture_source = agent`, which is authoritative: later Stop-hook heuristics only fill empty fields and background enrichment is skipped, so the capture is never clobbered.
|
|
27
|
+
- Modified-file lists are derived from `Create`/`Edit` `.tool_input.file_path`; `Execute` activity is summarized from `.tool_input.command`.
|
|
28
|
+
|
|
29
|
+
## Eagle Mem Files Depending On This
|
|
30
|
+
|
|
31
|
+
- `lib/factory-hooks.sh`
|
|
32
|
+
- `lib/common.sh`
|
|
33
|
+
- `lib/hooks-posttool.sh`
|
|
34
|
+
- `hooks/session-start.sh`
|
|
35
|
+
- `hooks/user-prompt-submit.sh`
|
|
36
|
+
- `hooks/pre-tool-use.sh`
|
|
37
|
+
- `hooks/post-tool-use.sh`
|
|
38
|
+
- `hooks/pre-compact.sh`
|
|
39
|
+
- `hooks/stop.sh`
|
|
40
|
+
- `scripts/install.sh`
|
|
41
|
+
- `scripts/update.sh`
|
|
42
|
+
- `scripts/uninstall.sh`
|
|
43
|
+
- `scripts/doctor.sh`
|
|
44
|
+
|
|
45
|
+
## Fixtures And Tests
|
|
46
|
+
|
|
47
|
+
- `tests/fixtures/agent-hooks/factory-post-tool-use.json`
|
|
48
|
+
- `tests/fixtures/agent-hooks/factory-create.json`
|
|
49
|
+
- `tests/test_factory_hooks_config.sh`
|
|
50
|
+
- `tests/test_agent_compatibility_docs_gate.sh`
|
|
51
|
+
|
|
52
|
+
## Reverification Notes
|
|
53
|
+
|
|
54
|
+
When editing Factory hooks or install/update behavior, update this file with the new verification date and source URLs. If a future Factory version changes the settings.json hook location, the `{type, command, timeout}` entry schema, matcher support, tool names (`Create`/`Execute`/`Edit`/`Read`), PreCompact matchers, or the transcript schema, update the fixtures first and then the implementation. Never write a `statusMessage` key into `settings.json`, and never register into a `hooks.json` file — Factory ignores it.
|
|
55
|
+
|
|
56
|
+
Registration deduplicates on `(matcher, command)` and updates in place when a command at the same `.eagle-mem/hooks/<script>.sh` path differs (e.g. a wrapper-prefix change). One consequence (shared with the Codex/OpenCode peers): if a future Eagle Mem release **changes an existing matcher string** — e.g. adds a tool name to `"Execute|Read|Edit|Create"` — the stored entry's old matcher matches neither the dedup nor the in-place-update query, so registration appends a second handler and `PostToolUse` would double-fire. When changing a Factory matcher string, strip the stale matcher-group first (or bump the removal path) so the installer cannot leave a duplicate. The transcript-line schema here is verified against the documented spec, not live Droid JSONL — when a future Factory version ships, diff a real `Stop` transcript against the union selectors before trusting capture.
|
package/hooks/post-tool-use.sh
CHANGED
|
@@ -163,7 +163,7 @@ esac
|
|
|
163
163
|
|
|
164
164
|
# Only track relevant tools
|
|
165
165
|
case "$tool_name" in
|
|
166
|
-
Read|Write|Edit|apply_patch|TaskUpdate) ;;
|
|
166
|
+
Read|Write|Edit|Create|apply_patch|TaskUpdate) ;;
|
|
167
167
|
*) eagle_is_shell_tool "$tool_name" || exit 0 ;;
|
|
168
168
|
esac
|
|
169
169
|
|
|
@@ -198,6 +198,11 @@ case "$tool_name" in
|
|
|
198
198
|
[ -n "$fp" ] && files_modified=$(printf '%s' "$fp" | jq -Rsc '[.]')
|
|
199
199
|
tool_summary="Write $fp"
|
|
200
200
|
;;
|
|
201
|
+
Create)
|
|
202
|
+
fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
|
203
|
+
[ -n "$fp" ] && files_modified=$(printf '%s' "$fp" | jq -Rsc '[.]')
|
|
204
|
+
tool_summary="Create $fp"
|
|
205
|
+
;;
|
|
201
206
|
Edit)
|
|
202
207
|
fp=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
|
203
208
|
[ -n "$fp" ] && files_modified=$(printf '%s' "$fp" | jq -Rsc '[.]')
|
|
@@ -215,7 +220,7 @@ case "$tool_name" in
|
|
|
215
220
|
tool_summary="apply_patch"
|
|
216
221
|
fi
|
|
217
222
|
;;
|
|
218
|
-
Bash|exec_command|shell_command|unified_exec)
|
|
223
|
+
Bash|exec_command|shell_command|unified_exec|Execute)
|
|
219
224
|
# Redact BEFORE truncating: a secret split across the 200-char boundary
|
|
220
225
|
# could otherwise survive because the redaction prefix patterns no longer
|
|
221
226
|
# see the full token.
|
|
@@ -265,7 +270,7 @@ esac
|
|
|
265
270
|
|
|
266
271
|
if [ -n "$session_id" ] && eagle_validate_session_id "$session_id"; then
|
|
267
272
|
case "$tool_name" in
|
|
268
|
-
Edit|Write|apply_patch)
|
|
273
|
+
Edit|Write|Create|apply_patch)
|
|
269
274
|
modified_paths=$(printf '%s' "$files_modified" | jq -r '.[]?' 2>/dev/null)
|
|
270
275
|
[ -n "$modified_paths" ] || modified_paths="$fp"
|
|
271
276
|
while IFS= read -r modified_path; do
|
|
@@ -280,7 +285,7 @@ fi
|
|
|
280
285
|
# ─── Enforced anti-regression: mark affected features pending ──
|
|
281
286
|
|
|
282
287
|
case "$tool_name" in
|
|
283
|
-
Edit|Write|apply_patch)
|
|
288
|
+
Edit|Write|Create|apply_patch)
|
|
284
289
|
if [ -n "$files_modified" ] && [ "$files_modified" != "[]" ]; then
|
|
285
290
|
while IFS= read -r mod_file; do
|
|
286
291
|
[ -z "$mod_file" ] && continue
|
package/hooks/pre-compact.sh
CHANGED
|
@@ -91,13 +91,14 @@ if [ "$existing_source" = "agent" ]; then
|
|
|
91
91
|
exit 0
|
|
92
92
|
fi
|
|
93
93
|
|
|
94
|
-
# Latest assistant text from the transcript (Claude
|
|
95
|
-
#
|
|
96
|
-
#
|
|
94
|
+
# Latest assistant text from the transcript (Claude top-level `assistant` and
|
|
95
|
+
# Factory `message`/role=assistant share the `.message.content` array). The same
|
|
96
|
+
# surface the Stop hook hands the enricher; enrich-summary.sh bounds it to a tail
|
|
97
|
+
# excerpt before sending it to the provider.
|
|
97
98
|
text_content=""
|
|
98
99
|
if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
99
100
|
text_content=$(jq -r -s '
|
|
100
|
-
[.[] | select(.type == "assistant")]
|
|
101
|
+
[.[] | select(.type == "assistant" or (.type == "message" and .message.role == "assistant"))]
|
|
101
102
|
| (.[-5:] // [])
|
|
102
103
|
| [.[] | .message.content[]? | select(.type == "text") | .text]
|
|
103
104
|
| join("\n")
|
package/hooks/pre-tool-use.sh
CHANGED
|
@@ -26,7 +26,7 @@ IFS=$'\x1f' read -r tool_name session_id cwd <<< \
|
|
|
26
26
|
agent=$(eagle_agent_source_from_json "$input")
|
|
27
27
|
|
|
28
28
|
case "$tool_name" in
|
|
29
|
-
Read|Edit|Write|apply_patch) ;;
|
|
29
|
+
Read|Edit|Write|Create|apply_patch) ;;
|
|
30
30
|
*) eagle_is_shell_tool "$tool_name" || exit 0 ;;
|
|
31
31
|
esac
|
|
32
32
|
|
|
@@ -39,7 +39,7 @@ context=""
|
|
|
39
39
|
updated_input=""
|
|
40
40
|
|
|
41
41
|
case "$tool_name" in
|
|
42
|
-
Bash|exec_command|shell_command|unified_exec)
|
|
42
|
+
Bash|exec_command|shell_command|unified_exec|Execute)
|
|
43
43
|
cmd=$(eagle_tool_command_from_json "$input")
|
|
44
44
|
[ -z "$cmd" ] && exit 0
|
|
45
45
|
|
|
@@ -213,7 +213,7 @@ ${context}================"
|
|
|
213
213
|
fi
|
|
214
214
|
;;
|
|
215
215
|
|
|
216
|
-
Edit|Write|apply_patch)
|
|
216
|
+
Edit|Write|Create|apply_patch)
|
|
217
217
|
target_files=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
|
218
218
|
if [ "$tool_name" = "apply_patch" ]; then
|
|
219
219
|
patch_cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
|
package/hooks/stop.sh
CHANGED
|
@@ -68,7 +68,7 @@ eagle_clean_request_candidates() {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
71
|
-
request=$(jq -r 'select(.type == "user") | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null \
|
|
71
|
+
request=$(jq -r 'select(.type == "user" or (.type == "message" and .message.role == "user")) | .message.content | if type == "string" then . elif type == "array" then [.[] | select(.type == "text") | .text] | join(" ") else "" end' "$transcript_path" 2>/dev/null \
|
|
72
72
|
| eagle_clean_request_candidates)
|
|
73
73
|
|
|
74
74
|
if [ -z "$request" ]; then
|
|
@@ -87,8 +87,8 @@ if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
|
87
87
|
| eagle_clean_request_candidates)
|
|
88
88
|
fi
|
|
89
89
|
|
|
90
|
-
heuristic_reads=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
|
|
91
|
-
heuristic_writes=$(jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
|
|
90
|
+
heuristic_reads=$(jq -r 'select(.type == "assistant" or (.type == "message" and .message.role == "assistant")) | .message.content[]? | select(.type == "tool_use") | select(.name == "Read") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
|
|
91
|
+
heuristic_writes=$(jq -r 'select(.type == "assistant" or (.type == "message" and .message.role == "assistant")) | .message.content[]? | select(.type == "tool_use") | select(.name == "Write" or .name == "Edit" or .name == "Create") | .input.file_path // empty' "$transcript_path" 2>/dev/null | sort -u | head -20)
|
|
92
92
|
fi
|
|
93
93
|
|
|
94
94
|
files_read="[]"
|
|
@@ -118,7 +118,7 @@ eagle_log "INFO" "Stop: heuristic extraction complete"
|
|
|
118
118
|
|
|
119
119
|
if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
|
|
120
120
|
text_content=$(jq -r -s '
|
|
121
|
-
[.[] | select(.type == "assistant")] | last |
|
|
121
|
+
[.[] | select(.type == "assistant" or (.type == "message" and .message.role == "assistant"))] | last |
|
|
122
122
|
if . then
|
|
123
123
|
[.message.content[]? | select(.type == "text") | .text] | join("\n")
|
|
124
124
|
else "" end
|
package/lib/common.sh
CHANGED
|
@@ -26,6 +26,8 @@ EAGLE_OPENCODE_CONFIG="${EAGLE_OPENCODE_CONFIG:-$EAGLE_OPENCODE_DIR/opencode.jso
|
|
|
26
26
|
EAGLE_OPENCODE_PLUGINS_DIR="${EAGLE_OPENCODE_PLUGINS_DIR:-$EAGLE_OPENCODE_DIR/plugins}"
|
|
27
27
|
EAGLE_OPENCODE_PLUGIN="${EAGLE_OPENCODE_PLUGIN:-$EAGLE_OPENCODE_PLUGINS_DIR/eagle-mem.js}"
|
|
28
28
|
EAGLE_OPENCODE_SKILLS_DIR="${EAGLE_OPENCODE_SKILLS_DIR:-$EAGLE_OPENCODE_DIR/skills}"
|
|
29
|
+
EAGLE_FACTORY_DIR="${EAGLE_FACTORY_DIR:-$HOME/.factory}"
|
|
30
|
+
EAGLE_FACTORY_SETTINGS="${EAGLE_FACTORY_SETTINGS:-$EAGLE_FACTORY_DIR/settings.json}"
|
|
29
31
|
EAGLE_RAW_BASH_UNLOCK="${EAGLE_RAW_BASH_UNLOCK:-/tmp/eagle-mem-raw-bash-unlock}"
|
|
30
32
|
|
|
31
33
|
_eagle_sqlite_candidate_paths() {
|
|
@@ -935,6 +937,7 @@ eagle_agent_source() {
|
|
|
935
937
|
codex|openai-codex) echo "codex" ;;
|
|
936
938
|
claude|claude-code|cloud-code) echo "claude-code" ;;
|
|
937
939
|
antigravity*|google-antigravity*|google_antigravity*) echo "antigravity" ;;
|
|
940
|
+
factory|factory-droid|droid) echo "factory" ;;
|
|
938
941
|
*)
|
|
939
942
|
if [ -n "${CODEX_THREAD_ID:-}" ] || [ -n "${CODEX_CI:-}" ] || [ -n "${CODEX_MANAGED_BY_NPM:-}" ]; then
|
|
940
943
|
echo "codex"
|
|
@@ -962,10 +965,12 @@ eagle_agent_source_from_json() {
|
|
|
962
965
|
case "$agent_field" in
|
|
963
966
|
antigravity*) echo "antigravity"; return ;;
|
|
964
967
|
opencode*) echo "opencode"; return ;;
|
|
968
|
+
factory*|droid*) echo "factory"; return ;;
|
|
965
969
|
esac
|
|
966
970
|
|
|
967
971
|
case "$transcript_path" in
|
|
968
972
|
"$HOME/.codex/"*|*/.codex/*) echo "codex"; return ;;
|
|
973
|
+
"$EAGLE_FACTORY_DIR/"*|*/.factory/*) echo "factory"; return ;;
|
|
969
974
|
"$HOME/.claude/"*|*/.claude/*) echo "claude-code"; return ;;
|
|
970
975
|
esac
|
|
971
976
|
[ -n "$turn_id" ] && { echo "codex"; return; }
|
|
@@ -979,6 +984,7 @@ eagle_agent_label() {
|
|
|
979
984
|
codex) echo "Codex" ;;
|
|
980
985
|
antigravity*) echo "Antigravity" ;;
|
|
981
986
|
opencode) echo "OpenCode" ;;
|
|
987
|
+
factory) echo "Factory Droid" ;;
|
|
982
988
|
*) echo "Claude Code" ;;
|
|
983
989
|
esac
|
|
984
990
|
}
|
|
@@ -1001,7 +1007,7 @@ eagle_trim_text() {
|
|
|
1001
1007
|
|
|
1002
1008
|
eagle_is_shell_tool() {
|
|
1003
1009
|
case "${1:-}" in
|
|
1004
|
-
Bash|exec_command|shell_command|unified_exec) return 0 ;;
|
|
1010
|
+
Bash|exec_command|shell_command|unified_exec|Execute) return 0 ;;
|
|
1005
1011
|
*) return 1 ;;
|
|
1006
1012
|
esac
|
|
1007
1013
|
}
|
|
@@ -1674,6 +1680,7 @@ eagle_runtime_change_plan() {
|
|
|
1674
1680
|
local claude_found="${3:-false}"
|
|
1675
1681
|
local codex_found="${4:-false}"
|
|
1676
1682
|
local opencode_found="${5:-false}"
|
|
1683
|
+
local factory_found="${6:-false}"
|
|
1677
1684
|
|
|
1678
1685
|
echo ""
|
|
1679
1686
|
echo -e " ${BOLD:-}What will change${RESET:-}"
|
|
@@ -1716,6 +1723,15 @@ eagle_runtime_change_plan() {
|
|
|
1716
1723
|
echo -e " ${DIM:-}-> OpenCode not detected; OpenCode plugin/skills skipped${RESET:-}"
|
|
1717
1724
|
fi
|
|
1718
1725
|
|
|
1726
|
+
if [ "$factory_found" = true ]; then
|
|
1727
|
+
echo -e " ${CYAN:-}->${RESET:-} Update Factory Droid"
|
|
1728
|
+
echo -e " ${DIM:-}settings:${RESET:-} $EAGLE_FACTORY_SETTINGS"
|
|
1729
|
+
echo -e " ${DIM:-}hooks: ${RESET:-} SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd, PreCompact"
|
|
1730
|
+
echo -e " ${DIM:-}mode: ${RESET:-} merge Eagle Mem hooks into settings.json; backup + 600 perms first"
|
|
1731
|
+
else
|
|
1732
|
+
echo -e " ${DIM:-}-> Factory Droid not detected; Factory hooks skipped${RESET:-}"
|
|
1733
|
+
fi
|
|
1734
|
+
|
|
1719
1735
|
if [ "$action" = "update" ]; then
|
|
1720
1736
|
echo -e " ${CYAN:-}->${RESET:-} Refresh installed version metadata"
|
|
1721
1737
|
fi
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ═══════════════════════════════════════════════════════════
|
|
3
|
+
# Eagle Mem — Factory Droid hook registration helpers
|
|
4
|
+
# Shared by install.sh, update.sh, and uninstall.sh
|
|
5
|
+
# ═══════════════════════════════════════════════════════════
|
|
6
|
+
# Factory Droid reads hooks from ~/.factory/settings.json (the SAME file that
|
|
7
|
+
# holds customModels, apiKey, statusLine). There is no separate hooks.json —
|
|
8
|
+
# Factory never reads one. The jq patch below rewrites only the .hooks subtree,
|
|
9
|
+
# leaving every sibling key (incl. secrets) untouched, and only commits the
|
|
10
|
+
# result when jq succeeds, so a malformed settings.json is never clobbered.
|
|
11
|
+
# ═══════════════════════════════════════════════════════════
|
|
12
|
+
[ -n "${_EAGLE_FACTORY_HOOKS_LOADED:-}" ] && return 0
|
|
13
|
+
_EAGLE_FACTORY_HOOKS_LOADED=1
|
|
14
|
+
|
|
15
|
+
eagle_factory_backup_file() {
|
|
16
|
+
local file="$1"
|
|
17
|
+
[ -f "$file" ] || return 0
|
|
18
|
+
if [ -z "${_EAGLE_FACTORY_BACKUP_DONE:-}" ]; then
|
|
19
|
+
local timestamp
|
|
20
|
+
timestamp=$(date +%Y%m%d%H%M%S)
|
|
21
|
+
cp "$file" "${file}.bak.${timestamp}" 2>/dev/null || true
|
|
22
|
+
_EAGLE_FACTORY_BACKUP_DONE=1
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
eagle_patch_factory_hook() {
|
|
27
|
+
local hooks_file="$1"
|
|
28
|
+
local event="$2"
|
|
29
|
+
local matcher="$3"
|
|
30
|
+
local command="$4"
|
|
31
|
+
local description="${5:-}"
|
|
32
|
+
local timeout="${6:-}"
|
|
33
|
+
local script_path="$command"
|
|
34
|
+
script_path="${script_path#EAGLE_AGENT_SOURCE=factory bash \"}"
|
|
35
|
+
script_path="${script_path#bash \"}"
|
|
36
|
+
script_path="${script_path%\"}"
|
|
37
|
+
|
|
38
|
+
mkdir -p "$(dirname "$hooks_file")"
|
|
39
|
+
eagle_factory_backup_file "$hooks_file"
|
|
40
|
+
if [ ! -f "$hooks_file" ]; then
|
|
41
|
+
printf '{"hooks":{}}\n' > "$hooks_file"
|
|
42
|
+
chmod 600 "$hooks_file" 2>/dev/null || true
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
local match_query
|
|
46
|
+
if [ -n "$matcher" ]; then
|
|
47
|
+
match_query='.hooks[$event][]? | select(.matcher == $matcher and (.hooks[]?.command == $command))'
|
|
48
|
+
else
|
|
49
|
+
match_query='.hooks[$event][]? | select((.matcher == null or .matcher == "") and (.hooks[]?.command == $command))'
|
|
50
|
+
fi
|
|
51
|
+
if jq -e --arg event "$event" --arg matcher "$matcher" --arg command "$command" "$match_query" "$hooks_file" &>/dev/null; then
|
|
52
|
+
[ -n "$description" ] && eagle_ok "$description ${DIM}(already registered)${RESET}"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if jq -e --arg event "$event" --arg matcher "$matcher" --arg script "$script_path" '
|
|
57
|
+
.hooks[$event][]?
|
|
58
|
+
| select((($matcher == "" and (.matcher == null or .matcher == "")) or .matcher == $matcher)
|
|
59
|
+
and any(.hooks[]?; (.command // "") | contains($script)))
|
|
60
|
+
' "$hooks_file" &>/dev/null; then
|
|
61
|
+
local tmp_existing
|
|
62
|
+
tmp_existing=$(mktemp)
|
|
63
|
+
jq --arg event "$event" --arg matcher "$matcher" --arg script "$script_path" --arg command "$command" '
|
|
64
|
+
.hooks[$event] |= map(
|
|
65
|
+
if ((($matcher == "" and (.matcher == null or .matcher == "")) or .matcher == $matcher)
|
|
66
|
+
and any(.hooks[]?; (.command // "") | contains($script)))
|
|
67
|
+
then .hooks |= map(if ((.command // "") | contains($script)) then .command = $command else . end)
|
|
68
|
+
else .
|
|
69
|
+
end
|
|
70
|
+
)
|
|
71
|
+
' "$hooks_file" > "$tmp_existing" && mv "$tmp_existing" "$hooks_file"
|
|
72
|
+
[ -n "$description" ] && eagle_ok "$description ${DIM}(updated)${RESET}"
|
|
73
|
+
return 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
local entry
|
|
77
|
+
# Factory's hook command schema is {type, command, timeout?} only — no
|
|
78
|
+
# statusMessage field (confirmed against factory-ai/factory hooks-reference).
|
|
79
|
+
# An unknown key here lands in the same settings.json as model config + apiKey
|
|
80
|
+
# and could break strict parsing, so we emit strictly the documented shape.
|
|
81
|
+
entry=$(jq -nc \
|
|
82
|
+
--arg m "$matcher" \
|
|
83
|
+
--arg c "$command" \
|
|
84
|
+
--arg timeout "$timeout" '
|
|
85
|
+
{
|
|
86
|
+
hooks: [
|
|
87
|
+
{
|
|
88
|
+
type: "command",
|
|
89
|
+
command: $c
|
|
90
|
+
}
|
|
91
|
+
+ (if $timeout == "" then {} else {timeout: ($timeout | tonumber)} end)
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
+ (if $m == "" then {} else {matcher: $m} end)')
|
|
95
|
+
|
|
96
|
+
local tmp
|
|
97
|
+
tmp=$(mktemp)
|
|
98
|
+
# Pass $event as data via --arg and index dynamically so the event name is
|
|
99
|
+
# never interpolated into the jq program string (injection-safe even if the
|
|
100
|
+
# caller ever passes an untrusted event name). Sibling top-level keys
|
|
101
|
+
# (customModels, apiKey, statusLine) are preserved by this targeted assign.
|
|
102
|
+
jq --argjson entry "$entry" --arg event "$event" \
|
|
103
|
+
'.hooks[$event] = ((.hooks[$event] // []) + [$entry])' "$hooks_file" > "$tmp" && mv "$tmp" "$hooks_file"
|
|
104
|
+
chmod 600 "$hooks_file" 2>/dev/null || true
|
|
105
|
+
[ -n "$description" ] && eagle_ok "$description"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
eagle_register_factory_hooks() {
|
|
109
|
+
eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "SessionStart" "" \
|
|
110
|
+
"EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/session-start.sh\"" \
|
|
111
|
+
"Factory SessionStart hook" \
|
|
112
|
+
"30"
|
|
113
|
+
|
|
114
|
+
eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "UserPromptSubmit" "" \
|
|
115
|
+
"EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/user-prompt-submit.sh\"" \
|
|
116
|
+
"Factory UserPromptSubmit hook" \
|
|
117
|
+
"30"
|
|
118
|
+
|
|
119
|
+
# Matchers use Factory's tool names: Execute (shell), Create (new file),
|
|
120
|
+
# Edit, Read. The hooks recognize these natively (see eagle_is_shell_tool
|
|
121
|
+
# and the Create/Execute cases in pre/post-tool-use.sh).
|
|
122
|
+
eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "PreToolUse" "Execute|Read|Edit|Create" \
|
|
123
|
+
"EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/pre-tool-use.sh\"" \
|
|
124
|
+
"Factory PreToolUse hook" \
|
|
125
|
+
"30"
|
|
126
|
+
|
|
127
|
+
eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "PostToolUse" "Read|Create|Edit|Execute" \
|
|
128
|
+
"EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/post-tool-use.sh\"" \
|
|
129
|
+
"Factory PostToolUse hook" \
|
|
130
|
+
"30"
|
|
131
|
+
|
|
132
|
+
eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "Stop" "" \
|
|
133
|
+
"EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/stop.sh\"" \
|
|
134
|
+
"Factory Stop hook" \
|
|
135
|
+
"60"
|
|
136
|
+
|
|
137
|
+
eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "SessionEnd" "" \
|
|
138
|
+
"EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/session-end.sh\"" \
|
|
139
|
+
"Factory SessionEnd hook" \
|
|
140
|
+
"30"
|
|
141
|
+
|
|
142
|
+
# PreCompact's matcher is the trigger itself ("manual" | "auto"); register
|
|
143
|
+
# both so auto-compaction — the gap users actually hit — also captures before
|
|
144
|
+
# the context window collapses. The hook reads `reason`/`trigger` from stdin.
|
|
145
|
+
eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "PreCompact" "manual" \
|
|
146
|
+
"EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/pre-compact.sh\"" \
|
|
147
|
+
"Factory PreCompact hook (manual)" \
|
|
148
|
+
"30"
|
|
149
|
+
|
|
150
|
+
eagle_patch_factory_hook "$EAGLE_FACTORY_SETTINGS" "PreCompact" "auto" \
|
|
151
|
+
"EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/pre-compact.sh\"" \
|
|
152
|
+
"Factory PreCompact hook (auto)" \
|
|
153
|
+
"30"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
eagle_remove_factory_hooks() {
|
|
157
|
+
local hooks_file="$EAGLE_FACTORY_SETTINGS"
|
|
158
|
+
[ -f "$hooks_file" ] || return 1
|
|
159
|
+
command -v jq &>/dev/null || return 1
|
|
160
|
+
|
|
161
|
+
eagle_factory_backup_file "$hooks_file"
|
|
162
|
+
|
|
163
|
+
local tmp
|
|
164
|
+
tmp=$(mktemp)
|
|
165
|
+
jq '
|
|
166
|
+
def without_eagle_mem_handlers:
|
|
167
|
+
.hooks = ((.hooks // [])
|
|
168
|
+
| map(select(((.command // "") | contains(".eagle-mem/hooks/")) | not)));
|
|
169
|
+
|
|
170
|
+
if .hooks then
|
|
171
|
+
.hooks |= with_entries(
|
|
172
|
+
.value = [
|
|
173
|
+
.value[]?
|
|
174
|
+
| without_eagle_mem_handlers
|
|
175
|
+
| select((.hooks // []) | length > 0)
|
|
176
|
+
]
|
|
177
|
+
| select(.value != [])
|
|
178
|
+
)
|
|
179
|
+
else . end
|
|
180
|
+
| if .hooks == {} then del(.hooks) else . end
|
|
181
|
+
' "$hooks_file" > "$tmp" && mv "$tmp" "$hooks_file"
|
|
182
|
+
chmod 600 "$hooks_file" 2>/dev/null || true
|
|
183
|
+
}
|
package/lib/hooks-posttool.sh
CHANGED
|
@@ -11,7 +11,7 @@ eagle_posttool_mirror_writes() {
|
|
|
11
11
|
local agent="${5:-$(eagle_agent_source)}"
|
|
12
12
|
|
|
13
13
|
case "$tool_name" in
|
|
14
|
-
Write|Edit|apply_patch)
|
|
14
|
+
Write|Edit|apply_patch|Create)
|
|
15
15
|
if [ -n "$fp" ]; then
|
|
16
16
|
case "$fp" in
|
|
17
17
|
*..*) ;; # path traversal — skip
|
|
@@ -48,7 +48,7 @@ eagle_posttool_stale_hint() {
|
|
|
48
48
|
local agent="${4:-$(eagle_agent_source)}"
|
|
49
49
|
|
|
50
50
|
case "$tool_name" in
|
|
51
|
-
Write|Edit|apply_patch)
|
|
51
|
+
Write|Edit|apply_patch|Create)
|
|
52
52
|
if [ -n "$fp" ]; then
|
|
53
53
|
local fname fname_stem
|
|
54
54
|
fname=$(basename "$fp")
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eagle-mem",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, OpenCode, Grok, and
|
|
3
|
+
"version": "4.16.0",
|
|
4
|
+
"description": "Shared memory, release guardrails, RTK token protection, and worker lanes for Claude Code, Codex, OpenCode, Grok, Google Antigravity, and Factory Droid",
|
|
5
5
|
"bin": {
|
|
6
6
|
"eagle-mem": "bin/eagle-mem"
|
|
7
7
|
},
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"codex",
|
|
30
30
|
"opencode",
|
|
31
31
|
"grok",
|
|
32
|
+
"factory-droid",
|
|
32
33
|
"memory",
|
|
33
34
|
"sqlite",
|
|
34
35
|
"fts5",
|
package/scripts/doctor.sh
CHANGED
|
@@ -14,6 +14,7 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
|
|
|
14
14
|
. "$SCRIPTS_DIR/style.sh"
|
|
15
15
|
. "$LIB_DIR/common.sh"
|
|
16
16
|
. "$LIB_DIR/opencode-hooks.sh"
|
|
17
|
+
. "$LIB_DIR/factory-hooks.sh"
|
|
17
18
|
|
|
18
19
|
mode="install-footprint"
|
|
19
20
|
json_output=false
|
|
@@ -140,6 +141,15 @@ if [ -f "$EAGLE_CODEX_HOOKS" ] && command -v jq >/dev/null 2>&1; then
|
|
|
140
141
|
fi
|
|
141
142
|
fi
|
|
142
143
|
|
|
144
|
+
factory_hooks="not found"
|
|
145
|
+
if [ -f "$EAGLE_FACTORY_SETTINGS" ] && command -v jq >/dev/null 2>&1; then
|
|
146
|
+
if jq -e '.hooks // {} | .. | objects | .command? // empty | select(test("eagle-mem|\\.eagle-mem"))' "$EAGLE_FACTORY_SETTINGS" >/dev/null 2>&1; then
|
|
147
|
+
factory_hooks="registered"
|
|
148
|
+
else
|
|
149
|
+
factory_hooks="not registered"
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
|
|
143
153
|
statusline_state="not configured"
|
|
144
154
|
statusline_command=""
|
|
145
155
|
if [ -f "$EAGLE_SETTINGS" ] && command -v jq >/dev/null 2>&1; then
|
|
@@ -184,6 +194,7 @@ if [ "$json_output" = true ]; then
|
|
|
184
194
|
--arg claude_hooks "$claude_hooks" \
|
|
185
195
|
--arg codex_hooks "$codex_hooks" \
|
|
186
196
|
--arg opencode_plugin "$opencode_plugin" \
|
|
197
|
+
--arg factory_hooks "$factory_hooks" \
|
|
187
198
|
--arg statusline "$statusline_state" \
|
|
188
199
|
--arg hooks_cmp "$hooks_cmp" \
|
|
189
200
|
--arg lib_cmp "$lib_cmp" \
|
|
@@ -202,7 +213,7 @@ if [ "$json_output" = true ]; then
|
|
|
202
213
|
versions:{package:$package_version, installed:$installed_version},
|
|
203
214
|
sqlite:{path:$sqlite_bin, version:$sqlite_version, fts5:$sqlite_fts5},
|
|
204
215
|
database:{exists:$db_exists, integrity:{status:$db_integrity_status, detail:$db_integrity_detail}},
|
|
205
|
-
hooks:{claude:$claude_hooks, codex:$codex_hooks, opencode:$opencode_plugin, statusline:$statusline},
|
|
216
|
+
hooks:{claude:$claude_hooks, codex:$codex_hooks, opencode:$opencode_plugin, factory:$factory_hooks, statusline:$statusline},
|
|
206
217
|
runtime_drift:{hooks:$hooks_cmp, lib:$lib_cmp, db:$db_cmp, scripts:$scripts_cmp},
|
|
207
218
|
manifest:{path:$manifest_path, status:$manifest_status, checked:$manifest_checked,
|
|
208
219
|
missing:$manifest_missing, drift:$manifest_drift, version:$manifest_version,
|
|
@@ -249,6 +260,7 @@ echo -e " ${BOLD}Hooks / Skills${RESET}"
|
|
|
249
260
|
eagle_kv "Claude Code:" "$claude_hooks"
|
|
250
261
|
eagle_kv "Codex:" "$codex_hooks"
|
|
251
262
|
eagle_kv "OpenCode:" "$opencode_plugin"
|
|
263
|
+
eagle_kv "Factory Droid:" "$factory_hooks"
|
|
252
264
|
[ -d "$EAGLE_GROK_DIR" ] && eagle_kv "Grok skills:" "$EAGLE_GROK_SKILLS_DIR"
|
|
253
265
|
eagle_kv "Statusline:" "$statusline_state"
|
|
254
266
|
echo ""
|
package/scripts/install.sh
CHANGED
|
@@ -51,6 +51,7 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
|
|
|
51
51
|
. "$LIB_DIR/hooks.sh"
|
|
52
52
|
. "$LIB_DIR/codex-hooks.sh"
|
|
53
53
|
. "$LIB_DIR/opencode-hooks.sh"
|
|
54
|
+
. "$LIB_DIR/factory-hooks.sh"
|
|
54
55
|
|
|
55
56
|
SETTINGS="$EAGLE_SETTINGS"
|
|
56
57
|
|
|
@@ -186,6 +187,7 @@ claude_found=false
|
|
|
186
187
|
codex_found=false
|
|
187
188
|
grok_found=false
|
|
188
189
|
opencode_found=false
|
|
190
|
+
factory_found=false
|
|
189
191
|
|
|
190
192
|
if [ -d "$HOME/.claude" ]; then
|
|
191
193
|
eagle_ok "Claude Code ${DIM}(~/.claude/)${RESET}"
|
|
@@ -218,10 +220,17 @@ else
|
|
|
218
220
|
eagle_warn "OpenCode not found ${DIM}(~/.config/opencode/ missing and opencode not on PATH)${RESET}"
|
|
219
221
|
fi
|
|
220
222
|
|
|
221
|
-
if [
|
|
223
|
+
if [ -d "$EAGLE_FACTORY_DIR" ] || command -v droid &>/dev/null; then
|
|
224
|
+
factory_found=true
|
|
225
|
+
eagle_ok "Factory Droid ${DIM}($EAGLE_FACTORY_DIR/)${RESET}"
|
|
226
|
+
else
|
|
227
|
+
eagle_warn "Factory Droid not found ${DIM}(~/.factory/ missing and droid not on PATH)${RESET}"
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
if [ "$claude_found" = false ] && [ "$codex_found" = false ] && [ "$grok_found" = false ] && [ "$opencode_found" = false ] && [ "$factory_found" = false ]; then
|
|
222
231
|
eagle_fail "No supported agent install found"
|
|
223
232
|
echo ""
|
|
224
|
-
eagle_dim "Install Claude Code, Codex, OpenCode, or ensure ~/.grok/ exists, then re-run."
|
|
233
|
+
eagle_dim "Install Claude Code, Codex, OpenCode, Factory Droid, or ensure ~/.grok/ exists, then re-run."
|
|
225
234
|
echo ""
|
|
226
235
|
exit 1
|
|
227
236
|
fi
|
|
@@ -232,7 +241,7 @@ if [ "$prereqs_ok" = false ]; then
|
|
|
232
241
|
exit 1
|
|
233
242
|
fi
|
|
234
243
|
|
|
235
|
-
eagle_runtime_change_plan "install" "$PACKAGE_DIR" "$claude_found" "$codex_found" "$opencode_found"
|
|
244
|
+
eagle_runtime_change_plan "install" "$PACKAGE_DIR" "$claude_found" "$codex_found" "$opencode_found" "$factory_found"
|
|
236
245
|
|
|
237
246
|
echo ""
|
|
238
247
|
|
|
@@ -307,6 +316,18 @@ else
|
|
|
307
316
|
eagle_info "OpenCode plugin skipped ${DIM}(OpenCode not detected)${RESET}"
|
|
308
317
|
fi
|
|
309
318
|
|
|
319
|
+
if [ "$factory_found" = true ]; then
|
|
320
|
+
if ! command -v jq &>/dev/null; then
|
|
321
|
+
eagle_warn "Factory hooks skipped ${DIM}(jq not found)${RESET}"
|
|
322
|
+
elif [ "$DRY_RUN" -eq 1 ]; then
|
|
323
|
+
eagle_info "Would register Factory Droid hooks"
|
|
324
|
+
else
|
|
325
|
+
eagle_register_factory_hooks
|
|
326
|
+
fi
|
|
327
|
+
else
|
|
328
|
+
eagle_info "Factory hooks skipped ${DIM}(Factory Droid not detected)${RESET}"
|
|
329
|
+
fi
|
|
330
|
+
|
|
310
331
|
# ─── Install skills ────────────────────────────────────────
|
|
311
332
|
|
|
312
333
|
if [ "$claude_found" = true ] && [ -d "$PACKAGE_DIR/skills" ]; then
|
|
@@ -498,6 +519,7 @@ eagle_kv "Hooks:" "$EAGLE_MEM_DIR/hooks/"
|
|
|
498
519
|
[ "$codex_found" = true ] && eagle_kv "Codex hooks:" "$EAGLE_CODEX_HOOKS"
|
|
499
520
|
[ "$grok_found" = true ] && eagle_kv "Grok skills:" "$EAGLE_GROK_SKILLS_DIR"
|
|
500
521
|
[ "$opencode_found" = true ] && eagle_kv "OpenCode plugin:" "$EAGLE_OPENCODE_PLUGIN"
|
|
522
|
+
[ "$factory_found" = true ] && eagle_kv "Factory settings:" "$EAGLE_FACTORY_SETTINGS"
|
|
501
523
|
eagle_kv "Antigravity Hook:" "$EAGLE_MEM_DIR/integrations/google_antigravity_hook.py"
|
|
502
524
|
|
|
503
525
|
echo ""
|
|
@@ -510,6 +532,9 @@ fi
|
|
|
510
532
|
if [ "$claude_found" = true ] || [ "$codex_found" = true ]; then
|
|
511
533
|
eagle_dim "Start a new Claude Code or Codex session — Eagle Mem will activate automatically."
|
|
512
534
|
fi
|
|
535
|
+
if [ "$factory_found" = true ]; then
|
|
536
|
+
eagle_dim "Start a new Factory Droid session — Eagle Mem hooks are registered in settings.json."
|
|
537
|
+
fi
|
|
513
538
|
echo ""
|
|
514
539
|
eagle_info "Google Antigravity SDK Integration:"
|
|
515
540
|
eagle_dim " To use Eagle Mem inside your Python Antigravity agents, simply import and register the hook:"
|
package/scripts/test.sh
CHANGED
|
@@ -77,6 +77,7 @@ run_check "Claude Stop Hook Registration (bash wrapper)" "bash \"$SCRIPTS_DIR/..
|
|
|
77
77
|
run_check "Codex Hooks Config (canonical features.hooks flag)" "bash \"$SCRIPTS_DIR/../tests/test_codex_hooks_config.sh\""
|
|
78
78
|
run_check "OpenCode Hooks Config (global plugin and skill registration)" "bash \"$SCRIPTS_DIR/../tests/test_opencode_hooks_config.sh\""
|
|
79
79
|
run_check "OpenCode Plugin Adapter (hook normalization)" "bash \"$SCRIPTS_DIR/../tests/test_opencode_plugin_adapter.sh\""
|
|
80
|
+
run_check "Factory Hooks Config (settings.json merge preserves secrets, no statusMessage, idempotent, clean removal)" "bash \"$SCRIPTS_DIR/../tests/test_factory_hooks_config.sh\""
|
|
80
81
|
run_check "Rust Migration Plan (compatibility-first contract)" "bash \"$SCRIPTS_DIR/../tests/test_rust_migration_plan.sh\""
|
|
81
82
|
run_check "Database Repair (safe preview and recovery failure path)" "bash \"$SCRIPTS_DIR/../tests/test_repair.sh\""
|
|
82
83
|
run_check "Trust Surfaces (DB integrity, JSON errors, statusline)" "bash \"$SCRIPTS_DIR/../tests/test_trust_surfaces.sh\""
|
package/scripts/uninstall.sh
CHANGED
|
@@ -12,6 +12,7 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
|
|
|
12
12
|
. "$LIB_DIR/common.sh"
|
|
13
13
|
. "$LIB_DIR/codex-hooks.sh"
|
|
14
14
|
. "$LIB_DIR/opencode-hooks.sh"
|
|
15
|
+
. "$LIB_DIR/factory-hooks.sh"
|
|
15
16
|
|
|
16
17
|
SETTINGS="$EAGLE_SETTINGS"
|
|
17
18
|
dry_run=false
|
|
@@ -73,6 +74,17 @@ else
|
|
|
73
74
|
eagle_info "OpenCode plugin not present or not Eagle Mem-owned"
|
|
74
75
|
fi
|
|
75
76
|
|
|
77
|
+
factory_settings_backup=""
|
|
78
|
+
if [ -f "$EAGLE_FACTORY_SETTINGS" ]; then
|
|
79
|
+
factory_settings_backup=$(eagle_backup_user_file "$EAGLE_FACTORY_SETTINGS" 2>/dev/null || true)
|
|
80
|
+
fi
|
|
81
|
+
if eagle_remove_factory_hooks; then
|
|
82
|
+
eagle_ok "Hooks removed from Factory settings.json"
|
|
83
|
+
[ -n "$factory_settings_backup" ] && eagle_dim " Backup: $factory_settings_backup"
|
|
84
|
+
else
|
|
85
|
+
eagle_info "Factory settings.json not present or jq unavailable"
|
|
86
|
+
fi
|
|
87
|
+
|
|
76
88
|
# ─── Remove instruction blocks and statusline integration ───
|
|
77
89
|
|
|
78
90
|
claude_md="$HOME/.claude/CLAUDE.md"
|
package/scripts/update.sh
CHANGED
|
@@ -17,12 +17,14 @@ LIB_DIR="$SCRIPTS_DIR/../lib"
|
|
|
17
17
|
. "$LIB_DIR/hooks.sh"
|
|
18
18
|
. "$LIB_DIR/codex-hooks.sh"
|
|
19
19
|
. "$LIB_DIR/opencode-hooks.sh"
|
|
20
|
+
. "$LIB_DIR/factory-hooks.sh"
|
|
20
21
|
|
|
21
22
|
SETTINGS="$EAGLE_SETTINGS"
|
|
22
23
|
claude_found=false
|
|
23
24
|
codex_found=false
|
|
24
25
|
grok_found=false
|
|
25
26
|
opencode_found=false
|
|
27
|
+
factory_found=false
|
|
26
28
|
|
|
27
29
|
eagle_header "Update"
|
|
28
30
|
|
|
@@ -48,8 +50,11 @@ fi
|
|
|
48
50
|
if eagle_opencode_detected; then
|
|
49
51
|
opencode_found=true
|
|
50
52
|
fi
|
|
53
|
+
if [ -d "$EAGLE_FACTORY_DIR" ] || command -v droid &>/dev/null; then
|
|
54
|
+
factory_found=true
|
|
55
|
+
fi
|
|
51
56
|
|
|
52
|
-
eagle_runtime_change_plan "update" "$PACKAGE_DIR" "$claude_found" "$codex_found" "$opencode_found"
|
|
57
|
+
eagle_runtime_change_plan "update" "$PACKAGE_DIR" "$claude_found" "$codex_found" "$opencode_found" "$factory_found"
|
|
53
58
|
|
|
54
59
|
# ─── Update files ──────────────────────────────────────────
|
|
55
60
|
|
|
@@ -105,6 +110,15 @@ else
|
|
|
105
110
|
eagle_info "OpenCode plugin skipped ${DIM}(OpenCode not detected)${RESET}"
|
|
106
111
|
fi
|
|
107
112
|
|
|
113
|
+
if [ "$factory_found" = true ] && command -v jq &>/dev/null; then
|
|
114
|
+
eagle_register_factory_hooks
|
|
115
|
+
eagle_ok "Factory Droid hooks registered"
|
|
116
|
+
elif [ "$factory_found" = true ]; then
|
|
117
|
+
eagle_warn "Factory hooks skipped ${DIM}(jq not found)${RESET}"
|
|
118
|
+
else
|
|
119
|
+
eagle_info "Factory hooks skipped ${DIM}(Factory Droid not detected)${RESET}"
|
|
120
|
+
fi
|
|
121
|
+
|
|
108
122
|
# ─── Update skill symlinks ────────────────────────────────
|
|
109
123
|
|
|
110
124
|
if [ "$claude_found" = true ] && [ -d "$PACKAGE_DIR/skills" ]; then
|
|
@@ -35,6 +35,7 @@ require_file "$DOC_DIR/README.md"
|
|
|
35
35
|
require_file "$DOC_DIR/claude-code.md"
|
|
36
36
|
require_file "$DOC_DIR/codex.md"
|
|
37
37
|
require_file "$DOC_DIR/opencode.md"
|
|
38
|
+
require_file "$DOC_DIR/factory.md"
|
|
38
39
|
require_file "$FIXTURE_DIR/claude-user-prompt-submit.json"
|
|
39
40
|
require_file "$FIXTURE_DIR/claude-statusline.json"
|
|
40
41
|
require_file "$FIXTURE_DIR/codex-user-prompt-submit.json"
|
|
@@ -44,6 +45,8 @@ require_file "$FIXTURE_DIR/opencode-tool-execute-before.json"
|
|
|
44
45
|
require_file "$FIXTURE_DIR/opencode-tool-execute-after.json"
|
|
45
46
|
require_file "$FIXTURE_DIR/opencode-todo-updated.json"
|
|
46
47
|
require_file "$FIXTURE_DIR/opencode-session-compacting.json"
|
|
48
|
+
require_file "$FIXTURE_DIR/factory-post-tool-use.json"
|
|
49
|
+
require_file "$FIXTURE_DIR/factory-create.json"
|
|
47
50
|
|
|
48
51
|
require_json "$FIXTURE_DIR/claude-user-prompt-submit.json"
|
|
49
52
|
require_json "$FIXTURE_DIR/claude-statusline.json"
|
|
@@ -54,6 +57,8 @@ require_json "$FIXTURE_DIR/opencode-tool-execute-before.json"
|
|
|
54
57
|
require_json "$FIXTURE_DIR/opencode-tool-execute-after.json"
|
|
55
58
|
require_json "$FIXTURE_DIR/opencode-todo-updated.json"
|
|
56
59
|
require_json "$FIXTURE_DIR/opencode-session-compacting.json"
|
|
60
|
+
require_json "$FIXTURE_DIR/factory-post-tool-use.json"
|
|
61
|
+
require_json "$FIXTURE_DIR/factory-create.json"
|
|
57
62
|
|
|
58
63
|
require_contains "$DOC_DIR/README.md" "Before modifying" "the required pre-edit workflow"
|
|
59
64
|
require_contains "$DOC_DIR/README.md" "official documentation" "official documentation"
|
|
@@ -91,6 +96,18 @@ require_contains "$DOC_DIR/opencode.md" "opencode --pure" "OpenCode pure-mode ca
|
|
|
91
96
|
require_contains "$DOC_DIR/opencode.md" "tests/fixtures/agent-hooks/opencode-tool-execute-before\\.json" "OpenCode PreToolUse fixture"
|
|
92
97
|
require_contains "$DOC_DIR/opencode.md" "tests/test_opencode_plugin_adapter\\.sh" "OpenCode adapter regression test"
|
|
93
98
|
|
|
99
|
+
require_contains "$DOC_DIR/factory.md" "Last verified: [0-9]{4}-[0-9]{2}-[0-9]{2}" "a Factory Last verified date"
|
|
100
|
+
require_contains "$DOC_DIR/factory.md" "https://docs\\.factory\\.ai/" "Factory documentation source"
|
|
101
|
+
require_contains "$DOC_DIR/factory.md" "github\\.com/factory-ai/factory" "Factory hooks guide source"
|
|
102
|
+
require_contains "$DOC_DIR/factory.md" "~/.factory/settings\\.json" "Factory hooks live in settings.json"
|
|
103
|
+
require_contains "$DOC_DIR/factory.md" "apiKey" "Factory settings.json secret co-location"
|
|
104
|
+
require_contains "$DOC_DIR/factory.md" "no .?statusMessage" "Factory schema has no statusMessage field"
|
|
105
|
+
require_contains "$DOC_DIR/factory.md" "Execute" "Factory Execute tool name"
|
|
106
|
+
require_contains "$DOC_DIR/factory.md" "Create" "Factory Create tool name"
|
|
107
|
+
require_contains "$DOC_DIR/factory.md" "PreCompact" "Factory PreCompact matchers"
|
|
108
|
+
require_contains "$DOC_DIR/factory.md" "tests/fixtures/agent-hooks/factory-post-tool-use\\.json" "Factory PostToolUse fixture"
|
|
109
|
+
require_contains "$DOC_DIR/factory.md" "tests/test_factory_hooks_config\\.sh" "Factory hooks config regression test"
|
|
110
|
+
|
|
94
111
|
if grep -R "features\\.codex_hooks" "$ROOT_DIR/lib" "$ROOT_DIR/scripts" "$ROOT_DIR/hooks" "$ROOT_DIR/bin" >/dev/null 2>&1; then
|
|
95
112
|
fail "implementation still uses deprecated Codex features.codex_hooks instead of features.hooks"
|
|
96
113
|
fi
|
|
@@ -104,7 +121,7 @@ changed_sensitive=$(
|
|
|
104
121
|
git -C "$ROOT_DIR" diff --name-only HEAD -- 2>/dev/null || true
|
|
105
122
|
git -C "$ROOT_DIR" ls-files --others --exclude-standard 2>/dev/null || true
|
|
106
123
|
} | awk '
|
|
107
|
-
/^(hooks\/.*\.sh|lib\/hooks\.sh|lib\/codex-hooks\.sh|lib\/opencode-hooks\.sh|integrations\/opencode_eagle_mem_plugin\.js|scripts\/install\.sh|scripts\/update\.sh|scripts\/uninstall\.sh|scripts\/doctor\.sh|scripts\/statusline-em\.sh|lib\/common\.sh|AGENTS\.md|CLAUDE\.md)$/ { print }
|
|
124
|
+
/^(hooks\/.*\.sh|lib\/hooks\.sh|lib\/codex-hooks\.sh|lib\/opencode-hooks\.sh|lib\/factory-hooks\.sh|integrations\/opencode_eagle_mem_plugin\.js|scripts\/install\.sh|scripts\/update\.sh|scripts\/uninstall\.sh|scripts\/doctor\.sh|scripts\/statusline-em\.sh|lib\/common\.sh|AGENTS\.md|CLAUDE\.md)$/ { print }
|
|
108
125
|
' | sort -u
|
|
109
126
|
)
|
|
110
127
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Factory Droid hooks live in ~/.factory/settings.json — the SAME file that
|
|
3
|
+
# holds apiKey, customModels, and statusLine. Registration must rewrite ONLY
|
|
4
|
+
# the .hooks subtree (preserving every secret-bearing sibling), emit strictly
|
|
5
|
+
# the documented {type, command, timeout} shape (no statusMessage), stay
|
|
6
|
+
# idempotent, and remove cleanly. This guards all of those invariants.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
10
|
+
|
|
11
|
+
command -v jq >/dev/null 2>&1 || { echo "factory hooks config: jq not found, skipping"; exit 0; }
|
|
12
|
+
|
|
13
|
+
tmp_dir=$(mktemp -d "$ROOT_DIR/.tmp-factory-hooks-config.XXXXXX")
|
|
14
|
+
trap 'rm -rf "$tmp_dir"' EXIT
|
|
15
|
+
|
|
16
|
+
# EAGLE_MEM_DIR mirrors the production layout ($HOME/.eagle-mem) so the command
|
|
17
|
+
# paths contain the literal ".eagle-mem/hooks/" substring that
|
|
18
|
+
# eagle_remove_factory_hooks matches on — otherwise the removal assertion below
|
|
19
|
+
# would pass vacuously against a path the real uninstaller would never strip.
|
|
20
|
+
export HOME="$tmp_dir/home"
|
|
21
|
+
export EAGLE_MEM_DIR="$HOME/.eagle-mem"
|
|
22
|
+
export EAGLE_FACTORY_DIR="$HOME/.factory"
|
|
23
|
+
export EAGLE_FACTORY_SETTINGS="$EAGLE_FACTORY_DIR/settings.json"
|
|
24
|
+
|
|
25
|
+
mkdir -p "$HOME" "$EAGLE_MEM_DIR" "$EAGLE_FACTORY_DIR"
|
|
26
|
+
|
|
27
|
+
. "$ROOT_DIR/scripts/style.sh"
|
|
28
|
+
. "$ROOT_DIR/lib/common.sh"
|
|
29
|
+
. "$ROOT_DIR/lib/factory-hooks.sh"
|
|
30
|
+
|
|
31
|
+
fail() {
|
|
32
|
+
echo "factory hooks config regression failed: $*" >&2
|
|
33
|
+
[ -f "$EAGLE_FACTORY_SETTINGS" ] && jq . "$EAGLE_FACTORY_SETTINGS" >&2 2>/dev/null || true
|
|
34
|
+
exit 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# ── Seed a realistic settings.json carrying secret-bearing siblings ─────────
|
|
38
|
+
# (FAKE values — never real credentials.)
|
|
39
|
+
cat > "$EAGLE_FACTORY_SETTINGS" <<'JSON'
|
|
40
|
+
{
|
|
41
|
+
"apiKey": "fake-sk-do-not-leak-0000",
|
|
42
|
+
"customModels": [
|
|
43
|
+
{ "model": "glm-5.2", "provider": "z-ai", "maxTokens": 200000 }
|
|
44
|
+
],
|
|
45
|
+
"statusLine": { "type": "command", "command": "echo droid" }
|
|
46
|
+
}
|
|
47
|
+
JSON
|
|
48
|
+
|
|
49
|
+
# ── 1. Register — siblings preserved, hooks added ───────────────────────────
|
|
50
|
+
eagle_register_factory_hooks >/dev/null
|
|
51
|
+
|
|
52
|
+
jq -e '.apiKey == "fake-sk-do-not-leak-0000"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
53
|
+
|| fail "apiKey was not preserved verbatim"
|
|
54
|
+
jq -e '.customModels[0].model == "glm-5.2" and .customModels[0].maxTokens == 200000' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
55
|
+
|| fail "customModels were not preserved"
|
|
56
|
+
jq -e '.statusLine.command == "echo droid"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
57
|
+
|| fail "statusLine was not preserved"
|
|
58
|
+
|
|
59
|
+
# Every registered Eagle handler points at .eagle-mem/hooks and uses type=command.
|
|
60
|
+
jq -e '
|
|
61
|
+
[.hooks | .. | objects | select(.command? != null)
|
|
62
|
+
| select(.command | test("\\.eagle-mem/hooks/"))]
|
|
63
|
+
| length == 8
|
|
64
|
+
and all(.[]; .type == "command")
|
|
65
|
+
' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
66
|
+
|| fail "expected 8 eagle command handlers across hook events"
|
|
67
|
+
|
|
68
|
+
# Every Eagle entry block carries a numeric timeout.
|
|
69
|
+
jq -e '
|
|
70
|
+
[.hooks | to_entries[] | .value[]
|
|
71
|
+
| select(any(.hooks[]?; .command | test("\\.eagle-mem/hooks/")))]
|
|
72
|
+
| all(.[]; all(.hooks[]; (.timeout | type) == "number"))
|
|
73
|
+
' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
74
|
+
|| fail "an Eagle hook entry is missing a numeric timeout"
|
|
75
|
+
|
|
76
|
+
# The documented schema has no statusMessage — an unknown key in this
|
|
77
|
+
# apiKey-bearing file risks strict-parse breakage, so it must never appear.
|
|
78
|
+
jq -e '[.hooks | .. | objects | select(has("statusMessage"))] | length == 0' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
79
|
+
|| fail "statusMessage key leaked into Factory settings.json"
|
|
80
|
+
|
|
81
|
+
# Matchers belong only to PreToolUse / PostToolUse / PreCompact.
|
|
82
|
+
jq -e '(.hooks.SessionStart[0] | has("matcher")) | not' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
83
|
+
|| fail "SessionStart should not carry a matcher"
|
|
84
|
+
jq -e '.hooks.PreToolUse[0].matcher | test("Execute") and test("Create")' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
85
|
+
|| fail "PreToolUse matcher missing Execute/Create"
|
|
86
|
+
jq -e '.hooks.PostToolUse[0].matcher | test("Execute") and test("Create")' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
87
|
+
|| fail "PostToolUse matcher missing Execute/Create"
|
|
88
|
+
jq -e '[.hooks.PreCompact[].matcher] | sort == ["auto","manual"]' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
89
|
+
|| fail "PreCompact should register both manual and auto matchers"
|
|
90
|
+
|
|
91
|
+
# ── 2. Idempotent — re-running adds no duplicates ───────────────────────────
|
|
92
|
+
eagle_register_factory_hooks >/dev/null
|
|
93
|
+
jq -e '
|
|
94
|
+
[.hooks | .. | objects | select(.command? != null)
|
|
95
|
+
| select(.command | test("\\.eagle-mem/hooks/"))]
|
|
96
|
+
| length == 8
|
|
97
|
+
' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
98
|
+
|| fail "re-registration created duplicate handlers (not idempotent)"
|
|
99
|
+
|
|
100
|
+
# ── 2b. Update branch — a stale command at the same script path is rewritten ──
|
|
101
|
+
# in place (not appended) to the canonical command, so the handler count stays 8.
|
|
102
|
+
# The idempotency check above only exercises the exact-match early return; this
|
|
103
|
+
# drives the contains($script) update path that a wrapper-prefix change would hit.
|
|
104
|
+
canonical_ss="EAGLE_AGENT_SOURCE=factory bash \"$EAGLE_MEM_DIR/hooks/session-start.sh\""
|
|
105
|
+
tmp_mut=$(mktemp)
|
|
106
|
+
jq --arg stale "STALE=1 bash \"$EAGLE_MEM_DIR/hooks/session-start.sh\"" \
|
|
107
|
+
'.hooks.SessionStart[0].hooks[0].command = $stale' \
|
|
108
|
+
"$EAGLE_FACTORY_SETTINGS" > "$tmp_mut" && mv "$tmp_mut" "$EAGLE_FACTORY_SETTINGS"
|
|
109
|
+
|
|
110
|
+
eagle_register_factory_hooks >/dev/null
|
|
111
|
+
|
|
112
|
+
jq -e --arg c "$canonical_ss" '.hooks.SessionStart[0].hooks[0].command == $c' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
113
|
+
|| fail "update branch did not rewrite a stale same-path command to canonical"
|
|
114
|
+
jq -e '
|
|
115
|
+
[.hooks | .. | objects | select(.command? != null)
|
|
116
|
+
| select(.command | test("\\.eagle-mem/hooks/"))]
|
|
117
|
+
| length == 8
|
|
118
|
+
' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
119
|
+
|| fail "update branch appended instead of updating in place (count != 8)"
|
|
120
|
+
|
|
121
|
+
# ── 3. Remove — hooks gone, secrets intact ──────────────────────────────────
|
|
122
|
+
eagle_remove_factory_hooks
|
|
123
|
+
jq -e 'has("hooks") | not' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
124
|
+
|| fail "hooks key was not removed after eagle_remove_factory_hooks"
|
|
125
|
+
jq -e '.apiKey == "fake-sk-do-not-leak-0000"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
126
|
+
|| fail "apiKey was lost during hook removal"
|
|
127
|
+
jq -e '.customModels[0].model == "glm-5.2"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
128
|
+
|| fail "customModels were lost during hook removal"
|
|
129
|
+
jq -e '.statusLine.command == "echo droid"' "$EAGLE_FACTORY_SETTINGS" >/dev/null \
|
|
130
|
+
|| fail "statusLine was lost during hook removal"
|
|
131
|
+
|
|
132
|
+
echo "factory hooks config regressions passed"
|