@gi4nks/ph 1.3.2
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 +57 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/cli.js +3885 -0
- package/dist/cli.js.map +1 -0
- package/docs/hooks.md +83 -0
- package/docs/ph-manual.md +283 -0
- package/hooks/claude/README.md +45 -0
- package/hooks/claude/ph-hook.sh +103 -0
- package/hooks/gemini/README.md +43 -0
- package/hooks/gemini/ph-hook.sh +56 -0
- package/package.json +55 -0
package/docs/hooks.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# ph Implementation Status — Complete Report
|
|
2
|
+
|
|
3
|
+
✅ Completed
|
|
4
|
+
|
|
5
|
+
┌───────────────────────────────────────────────────────────┬────────────────────────────┬────────┐
|
|
6
|
+
│ Feature │ File │ Status │
|
|
7
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
8
|
+
│ Git Context Snapshot │ src/runner/git-context.ts │ ✅ │
|
|
9
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
10
|
+
│ Background Analysis │ src/background/analyzer.ts │ ✅ │
|
|
11
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
12
|
+
│ Work Sessions (ph sessions) │ src/sessions/index.ts │ ✅ │
|
|
13
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
14
|
+
│ Prompt Rerunning (TUI r) │ src/ui/BrowseApp.tsx │ ✅ │
|
|
15
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
16
|
+
│ Ollama embeddings (nomic-embed-text-v2-moe) │ src/embedding/index.ts │ ✅ │
|
|
17
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
18
|
+
│ Response column in DB + FTS5 migration │ src/db/index.ts │ ✅ │
|
|
19
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
20
|
+
│ ph log command (flag + stdin JSON) │ src/cli.ts │ ✅ │
|
|
21
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
22
|
+
│ TUI detail view with response │ src/ui/BrowseApp.tsx │ ✅ │
|
|
23
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
24
|
+
│ Export markdown with response + git context │ src/cli.ts │ ✅ │
|
|
25
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
26
|
+
│ Gemini importer response capture │ src/importer/gemini.ts │ ✅ │
|
|
27
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
28
|
+
│ Claude importer from transcript files │ src/importer/claude.ts │ ✅ │
|
|
29
|
+
├───────────────────────────────────────────────────────────┼────────────────────────────┼────────┤
|
|
30
|
+
│ Documentation (README, CLAUDE.md, GEMINI.md, ROADMAP.md) │ various │ ✅ │
|
|
31
|
+
└───────────────────────────────────────────────────────────┴────────────────────────────┴────────┘
|
|
32
|
+
|
|
33
|
+
🔄 In Progress (background agent)
|
|
34
|
+
|
|
35
|
+
- Claude Code hook (~/.claude/settings.json + ~/.claude/ph-hook.sh): Stop hook that calls ph log after each response
|
|
36
|
+
- Gemini CLI skill/hook: similar integration for the Gemini CLI
|
|
37
|
+
|
|
38
|
+
📋 Hook Architecture (being implemented)
|
|
39
|
+
|
|
40
|
+
Claude finishes a response
|
|
41
|
+
→ Stop hook in settings.json
|
|
42
|
+
→ ~/.claude/ph-hook.sh receives JSON on stdin
|
|
43
|
+
→ jq extracts last user prompt + assistant response text
|
|
44
|
+
→ ph log --tool claude --prompt "..." --response "..."
|
|
45
|
+
→ record inserted into DB with background analysis
|
|
46
|
+
|
|
47
|
+
⏳ Still To Do (after hooks)
|
|
48
|
+
|
|
49
|
+
- Claude importer from history.jsonl: already replaced by the new transcript importer — ✅ covered
|
|
50
|
+
- End-to-end testing of hooks in a real session
|
|
51
|
+
- ph import claude update in cli.ts: verify that cmdImport calls the new importer correctly (the file was rewritten but the caller in cli.ts might still reference history.jsonl in the output log)
|
|
52
|
+
|
|
53
|
+
Final Data Flow
|
|
54
|
+
|
|
55
|
+
Gemini CLI / Claude Code
|
|
56
|
+
↓ (automatic hook after each exchange)
|
|
57
|
+
ph log (stdin JSON)
|
|
58
|
+
↓
|
|
59
|
+
SQLite (~/.prompt_history.db)
|
|
60
|
+
↓
|
|
61
|
+
background analysis (Ollama gemma3:1b)
|
|
62
|
+
↓
|
|
63
|
+
metadata: project, language, role, tags, relevance
|
|
64
|
+
↓
|
|
65
|
+
ph browse / ph search / ph sessions
|
|
66
|
+
|
|
67
|
+
The main agent is completing the hook part — once finished you will have the completely automatic system without having to use ph as a wrapper.
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
⏺ Everything is set. Summary of what has been done:
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
Claude Code — Stop hook registered in ~/.claude/settings.json:
|
|
74
|
+
- Reads the session's JSONL transcript
|
|
75
|
+
- Extracts the last user prompt (those with promptId, not tool-result) and the last assistant response
|
|
76
|
+
- Calls ph log in the background (disown) → never blocks Claude
|
|
77
|
+
|
|
78
|
+
Gemini CLI — AfterAgent hook registered in ~/.gemini/settings.json:
|
|
79
|
+
- Directly receives the JSON payload containing prompt and prompt_response from Gemini CLI
|
|
80
|
+
- Extracts the user prompt and the assistant response
|
|
81
|
+
- Calls ph log in the background → never blocks Gemini
|
|
82
|
+
|
|
83
|
+
From this moment, every exchange in Claude Code or Gemini CLI is automatically saved in ph without the need to use it as a wrapper.
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# ph — Prompt History CLI
|
|
2
|
+
|
|
3
|
+
**Architecture, Hook System & Usage Guide**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Overview
|
|
8
|
+
|
|
9
|
+
ph is a transparent capture layer for AI CLI tools such as Claude Code and Gemini CLI. Every prompt you type — and the AI response that follows — is automatically saved to a local SQLite database with full-text and semantic search, Git context, and optional background analysis.
|
|
10
|
+
|
|
11
|
+
The core design principle is zero friction: ph never interrupts your workflow. Capture happens silently in the background. Your AI sessions remain unchanged.
|
|
12
|
+
|
|
13
|
+
### Key Features
|
|
14
|
+
|
|
15
|
+
- **Automatic Capture** — saves prompts, responses, and metadata without any manual action
|
|
16
|
+
- **Git Context** — records current branch, modified files, and diff at capture time
|
|
17
|
+
- **Full-text & Semantic Search** — FTS5 text search and vector-based semantic search (via Ollama embeddings)
|
|
18
|
+
- **Background Analysis** — automatic role/tag/relevance classification via Ollama or Gemini
|
|
19
|
+
- **Interactive TUI** — browse, filter, edit, re-run, and export your history with `ph browse`
|
|
20
|
+
- **Privacy First** — all data stays local; no data ever leaves your machine
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Architecture
|
|
25
|
+
|
|
26
|
+
ph uses a two-tier storage model: a SQLite database with FTS5 full-text indexing for fast retrieval, and a JSON config file for runtime settings. Both live in your home directory by default.
|
|
27
|
+
|
|
28
|
+
### Storage Layout
|
|
29
|
+
|
|
30
|
+
| Item | Default Path | Description |
|
|
31
|
+
|------|-------------|-------------|
|
|
32
|
+
| Database | `~/.prompt_history.db` | SQLite with FTS5 + vector embeddings |
|
|
33
|
+
| Config | `~/.ph_config.json` | JSON config file (0o600 permissions) |
|
|
34
|
+
|
|
35
|
+
The database uses WAL (Write-Ahead Logging) mode, enabling concurrent reads and writes. This is essential for the background analysis system, which processes prompts asynchronously without blocking your terminal session.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 3. The Hook System
|
|
40
|
+
|
|
41
|
+
The hook system is ph's most powerful integration mechanism. Rather than requiring you to manually invoke ph for each session, hooks let the AI CLI tools themselves notify ph after every interaction — automatically, silently, and without any impact on response latency.
|
|
42
|
+
|
|
43
|
+
> **Key insight:** Hooks are post-action handlers registered inside the AI tool's own config. ph never wraps or intercepts the AI process. The AI runs normally; the hook fires after the session ends.
|
|
44
|
+
|
|
45
|
+
### How Hooks Work
|
|
46
|
+
|
|
47
|
+
Each supported AI CLI tool exposes a native hook mechanism:
|
|
48
|
+
|
|
49
|
+
- **Claude Code** — provides a `Stop` hook, triggered after each agent session completes
|
|
50
|
+
- **Gemini CLI** — provides an `AfterAgent` hook, triggered after each agent turn
|
|
51
|
+
|
|
52
|
+
When the hook fires, the AI tool passes a JSON payload on stdin to your registered script. The script reads that payload, extracts the prompt and response, and pipes the data to `ph log` — which persists everything to the local SQLite database.
|
|
53
|
+
|
|
54
|
+
The entire operation runs in the background using shell job control (`& + disown`), so it never adds latency to your terminal session and never blocks the AI tool from exiting.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### Claude Code Hook
|
|
59
|
+
|
|
60
|
+
#### How Claude Code delivers data
|
|
61
|
+
|
|
62
|
+
When the `Stop` hook fires, Claude Code sends a JSON object on stdin:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"session_id": "abc123...",
|
|
67
|
+
"transcript_path": "/path/to/session.jsonl"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The transcript is a JSONL file — one JSON object per line — containing the full session history. Each line is one of:
|
|
72
|
+
|
|
73
|
+
| Turn type | Identifier |
|
|
74
|
+
|-----------|-----------|
|
|
75
|
+
| Human turn | Has `promptId` key; `message.content` is a plain string |
|
|
76
|
+
| Tool result | Has `toolUseResult` key; `message.content` is an array |
|
|
77
|
+
| Assistant turn | `message.role == "assistant"`; content is an array of typed blocks |
|
|
78
|
+
|
|
79
|
+
#### What the hook script does
|
|
80
|
+
|
|
81
|
+
1. Reads the hook input from stdin to obtain the transcript path
|
|
82
|
+
2. Uses `jq` with slurp mode to parse the entire JSONL in one pass
|
|
83
|
+
3. Filters for genuine human turns: must have `promptId`, string content, and no `toolUseResult` key
|
|
84
|
+
4. Extracts the last human turn as the prompt
|
|
85
|
+
5. Scans assistant turns for the last `text`-type block as the response
|
|
86
|
+
6. Builds a JSON payload with `tool`, `prompt`, `response`, and `workdir` fields
|
|
87
|
+
7. Pipes the payload to `ph log` running in the background (`& + disown`)
|
|
88
|
+
8. Exits `0` unconditionally — Claude Code is never blocked
|
|
89
|
+
|
|
90
|
+
#### Hook installation — Claude Code
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Step 1: link the hook script
|
|
94
|
+
ln -sf /path/to/ph/hooks/claude/ph-hook.sh ~/.claude/ph-hook.sh
|
|
95
|
+
|
|
96
|
+
# Step 2: register in ~/.claude/settings.json
|
|
97
|
+
{
|
|
98
|
+
"hooks": {
|
|
99
|
+
"Stop": [
|
|
100
|
+
{
|
|
101
|
+
"matcher": ".*",
|
|
102
|
+
"hooks": [
|
|
103
|
+
{
|
|
104
|
+
"type": "command",
|
|
105
|
+
"command": "~/.claude/ph-hook.sh",
|
|
106
|
+
"async": true
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### Gemini CLI Hook
|
|
118
|
+
|
|
119
|
+
#### How Gemini CLI delivers data
|
|
120
|
+
|
|
121
|
+
Gemini CLI's `AfterAgent` hook is simpler: it delivers the prompt and response directly in the stdin payload, with no transcript file to parse:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"session_id": "...",
|
|
126
|
+
"transcript_path": "...",
|
|
127
|
+
"cwd": "/path/to/workdir",
|
|
128
|
+
"hook_event_name": "AfterAgent",
|
|
129
|
+
"timestamp": "...",
|
|
130
|
+
"prompt": "the user's prompt text",
|
|
131
|
+
"prompt_response": "the assistant's full response text"
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### What the hook script does
|
|
136
|
+
|
|
137
|
+
1. Reads the JSON payload from stdin
|
|
138
|
+
2. Extracts `prompt`, `prompt_response`, and `cwd` directly via `jq`
|
|
139
|
+
3. Builds a JSON payload with `tool`, `prompt`, `response`, and `workdir` fields
|
|
140
|
+
4. Pipes to `ph log` in the background
|
|
141
|
+
5. Exits `0` unconditionally
|
|
142
|
+
|
|
143
|
+
#### Hook installation — Gemini CLI
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Step 1: link the hook script
|
|
147
|
+
ln -sf /path/to/ph/hooks/gemini/ph-hook.sh ~/.gemini/ph-hook.sh
|
|
148
|
+
|
|
149
|
+
# Step 2: register in ~/.gemini/settings.json
|
|
150
|
+
{
|
|
151
|
+
"hooks": {
|
|
152
|
+
"AfterAgent": [
|
|
153
|
+
{
|
|
154
|
+
"hooks": [
|
|
155
|
+
{
|
|
156
|
+
"type": "command",
|
|
157
|
+
"command": "~/.gemini/ph-hook.sh"
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### Hook Comparison
|
|
169
|
+
|
|
170
|
+
| Aspect | Claude Code | Gemini CLI |
|
|
171
|
+
|--------|------------|------------|
|
|
172
|
+
| Hook name | `Stop` | `AfterAgent` |
|
|
173
|
+
| Data delivery | `transcript_path` (JSONL file) | Direct JSON on stdin |
|
|
174
|
+
| Prompt extraction | Parse JSONL with `jq` slurp | `jq .prompt` field |
|
|
175
|
+
| Response extraction | Last assistant text block | `jq .prompt_response` field |
|
|
176
|
+
| Async support | Yes (`async: true` in config) | Yes (`& + disown` in script) |
|
|
177
|
+
| Blocks AI exit? | Never (exits `0`) | Never (exits `0`) |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 4. Installation
|
|
182
|
+
|
|
183
|
+
### From Source
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
git clone git@github.com:gi4nks/ph.git
|
|
187
|
+
cd ph
|
|
188
|
+
npm install
|
|
189
|
+
make build
|
|
190
|
+
make install # installs the global 'ph' symlink
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Via npm
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Install globally:
|
|
197
|
+
npm install -g @gi4nks/ph
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Initial Configuration
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Set your Gemini API key (optional — Ollama works without it)
|
|
204
|
+
ph config set gemini-api-key "YOUR_KEY"
|
|
205
|
+
|
|
206
|
+
# Choose your analysis provider
|
|
207
|
+
ph config set analyze-provider ollama # default, fully local
|
|
208
|
+
# ph config set analyze-provider gemini # cloud alternative
|
|
209
|
+
|
|
210
|
+
# Enable background analysis
|
|
211
|
+
ph config set background-analysis true
|
|
212
|
+
|
|
213
|
+
# Optional: move the database to a custom location
|
|
214
|
+
ph config set db-path "/path/to/your/custom.db"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 5. Usage
|
|
220
|
+
|
|
221
|
+
### Search
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Full-text search
|
|
225
|
+
ph search "how to refactor"
|
|
226
|
+
|
|
227
|
+
# Semantic search (vector similarity)
|
|
228
|
+
ph search --semantic "refactoring patterns"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Interactive TUI
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
ph browse
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Available hotkeys inside the TUI:
|
|
238
|
+
|
|
239
|
+
| Key | Action |
|
|
240
|
+
|-----|--------|
|
|
241
|
+
| `f` | Open filters panel |
|
|
242
|
+
| `e` | Edit prompt metadata (tags, notes) |
|
|
243
|
+
| `r` | Re-run selected prompt in your AI tool |
|
|
244
|
+
| `y` | Copy prompt to clipboard |
|
|
245
|
+
| `x` | Delete prompt from history |
|
|
246
|
+
|
|
247
|
+
### Export
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# Export last 5 prompts as Markdown
|
|
251
|
+
ph export --format md --limit 5 > prompts.md
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Database Maintenance
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Remove short or old prompts
|
|
258
|
+
ph cleanup --min-length 20 --days 30
|
|
259
|
+
|
|
260
|
+
# Compact the database
|
|
261
|
+
ph vacuum
|
|
262
|
+
|
|
263
|
+
# Dry-run to preview what would be removed
|
|
264
|
+
ph cleanup --dry-run --days 60
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 6. Why Prompt History Matters
|
|
270
|
+
|
|
271
|
+
Every interaction with an AI tool is a unit of intellectual work. A well-crafted prompt encodes context, intent, and engineering decisions that took time to develop. Without capture, that work is lost the moment the session closes.
|
|
272
|
+
|
|
273
|
+
ph treats your prompt history as a first-class artifact: searchable, annotated, versioned with Git context, and available for re-use. Over time, your history becomes a personal knowledge base of how you think and work with AI tools.
|
|
274
|
+
|
|
275
|
+
> **The problem:** Manually saving prompts takes time, breaks flow, and rarely happens consistently. Without structure, even saved prompts become unsearchable noise.
|
|
276
|
+
|
|
277
|
+
> **The solution:** ph captures everything automatically via hooks, indexes it locally, and gives you semantic search so you can find what you need — even if you only remember the intent, not the exact wording.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Repository
|
|
282
|
+
|
|
283
|
+
[https://github.com/gi4nks/ph](https://github.com/gi4nks/ph)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Claude Code Hook for ph
|
|
2
|
+
|
|
3
|
+
This hook allows Claude Code to automatically save every prompt and response to the `ph` database.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
1. Ensure `ph` is installed and working.
|
|
8
|
+
2. Create a symbolic link of the script in Claude's configuration folder:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
ln -sf "$(pwd)/ph-hook.sh" ~/.claude/ph-hook.sh
|
|
12
|
+
chmod +x ~/.claude/ph-hook.sh
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Claude Code Configuration
|
|
16
|
+
|
|
17
|
+
Add (or update) the `hooks` section in your `~/.claude/settings.json` file:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"hooks": {
|
|
22
|
+
"Stop": [
|
|
23
|
+
{
|
|
24
|
+
"matcher": ".*",
|
|
25
|
+
"hooks": [
|
|
26
|
+
{
|
|
27
|
+
"type": "command",
|
|
28
|
+
"command": "/Users/gianluca/.claude/ph-hook.sh",
|
|
29
|
+
"async": true
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Note:** Ensure the path in the command correctly points to the symbolic link or the actual script. Using the symbolic link at `~/.claude/ph-hook.sh` keeps the configuration portable.
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
The script is executed by Claude Code at the `Stop` event (end of an exchange).
|
|
43
|
+
1. Reads the current session transcript.
|
|
44
|
+
2. Extracts the last user prompt and the last assistant response.
|
|
45
|
+
3. Sends the data to `ph log` in the background.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ph-hook.sh — Claude Code Stop hook: logs the last prompt+response pair to ph
|
|
3
|
+
#
|
|
4
|
+
# Claude Code passes a JSON object on stdin with:
|
|
5
|
+
# { "session_id": "...", "transcript_path": "/path/to/session.jsonl" }
|
|
6
|
+
#
|
|
7
|
+
# The transcript is a JSONL file where each line is one of:
|
|
8
|
+
# - Human turn: { "promptId": "...", "message": {"role":"user", "content": "string"}, "cwd": "...", ... }
|
|
9
|
+
# - Tool result: { "toolUseResult": ..., "message": {"role":"user", "content": [...]}, ... }
|
|
10
|
+
# - Assistant turn: { "message": {"role":"assistant", "content": [{type,text},...] }, ... }
|
|
11
|
+
#
|
|
12
|
+
# This script:
|
|
13
|
+
# 1. Reads the hook input to get transcript_path
|
|
14
|
+
# 2. Uses jq to find the last genuine human-typed user message (has promptId, string content)
|
|
15
|
+
# 3. Extracts the last assistant text block
|
|
16
|
+
# 4. Pipes {"tool":"claude","prompt":"...","response":"...","workdir":"..."} to ph log
|
|
17
|
+
# 5. Always exits 0 so Claude Code is never blocked
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
# ── Read hook input ──────────────────────────────────────────────────────────
|
|
22
|
+
HOOK_INPUT=$(cat)
|
|
23
|
+
|
|
24
|
+
TRANSCRIPT_PATH=$(printf '%s' "$HOOK_INPUT" | jq -r '.transcript_path // empty')
|
|
25
|
+
|
|
26
|
+
# Nothing to do if no transcript path provided
|
|
27
|
+
if [[ -z "$TRANSCRIPT_PATH" ]] || [[ ! -f "$TRANSCRIPT_PATH" ]]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# ── Extract last user prompt using jq slurp ──────────────────────────────────
|
|
32
|
+
# Filter for genuine human turns: has "promptId" AND message.content is a string
|
|
33
|
+
# (tool result lines also have role:user but have "toolUseResult" key and array content)
|
|
34
|
+
LAST_USER_JSON=$(
|
|
35
|
+
jq -Rs '
|
|
36
|
+
split("\n")
|
|
37
|
+
| map(select(length > 0))
|
|
38
|
+
| map(. as $line | try (. | fromjson) catch null)
|
|
39
|
+
| map(select(. != null))
|
|
40
|
+
| map(select(
|
|
41
|
+
has("promptId")
|
|
42
|
+
and has("message")
|
|
43
|
+
and (.message.role == "user")
|
|
44
|
+
and (.message.content | type == "string")
|
|
45
|
+
and (has("toolUseResult") | not)
|
|
46
|
+
))
|
|
47
|
+
| last
|
|
48
|
+
' "$TRANSCRIPT_PATH" 2>/dev/null
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if [[ -z "$LAST_USER_JSON" ]] || [[ "$LAST_USER_JSON" == "null" ]]; then
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
LAST_USER_PROMPT=$(printf '%s' "$LAST_USER_JSON" | jq -r '.message.content // empty' 2>/dev/null)
|
|
56
|
+
WORKDIR=$(printf '%s' "$LAST_USER_JSON" | jq -r '.cwd // empty' 2>/dev/null)
|
|
57
|
+
|
|
58
|
+
# Skip if no user prompt found
|
|
59
|
+
if [[ -z "$LAST_USER_PROMPT" ]]; then
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if [[ -z "$WORKDIR" ]]; then
|
|
64
|
+
WORKDIR="$HOME"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# ── Extract last assistant text response ─────────────────────────────────────
|
|
68
|
+
# Assistant messages have message.content as an array of typed blocks.
|
|
69
|
+
# We want the last "text" block across recent assistant messages.
|
|
70
|
+
LAST_ASSISTANT_RESPONSE=$(
|
|
71
|
+
jq -Rs '
|
|
72
|
+
split("\n")
|
|
73
|
+
| map(select(length > 0))
|
|
74
|
+
| map(. as $line | try (. | fromjson) catch null)
|
|
75
|
+
| map(select(. != null))
|
|
76
|
+
| map(select(
|
|
77
|
+
has("message")
|
|
78
|
+
and (.message.role == "assistant")
|
|
79
|
+
and (.message.content | type == "array")
|
|
80
|
+
))
|
|
81
|
+
| map(.message.content | map(select(.type == "text") | .text) | join(""))
|
|
82
|
+
| map(select(length > 0))
|
|
83
|
+
| last // ""
|
|
84
|
+
' "$TRANSCRIPT_PATH" 2>/dev/null | jq -r '.' 2>/dev/null
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# ── Call ph log ───────────────────────────────────────────────────────────────
|
|
88
|
+
# Use jq to safely build the JSON payload (handles escaping automatically)
|
|
89
|
+
JSON_PAYLOAD=$(
|
|
90
|
+
jq -n \
|
|
91
|
+
--arg tool "claude" \
|
|
92
|
+
--arg prompt "$LAST_USER_PROMPT" \
|
|
93
|
+
--arg response "${LAST_ASSISTANT_RESPONSE:-}" \
|
|
94
|
+
--arg workdir "$WORKDIR" \
|
|
95
|
+
'{"tool":$tool,"prompt":$prompt,"response":$response,"workdir":$workdir}'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Run ph log in the background so it never blocks Claude Code's exit
|
|
99
|
+
# Redirect all output to /dev/null to keep things silent
|
|
100
|
+
printf '%s' "$JSON_PAYLOAD" | ph log >/dev/null 2>&1 &
|
|
101
|
+
disown $! 2>/dev/null || true
|
|
102
|
+
|
|
103
|
+
exit 0
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Gemini CLI Hook for ph
|
|
2
|
+
|
|
3
|
+
This hook allows Gemini CLI to automatically save every prompt and response to the `ph` database.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
1. Ensure `ph` is installed and working.
|
|
8
|
+
2. Create a symbolic link of the script in Gemini's configuration folder:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
ln -sf "$(pwd)/ph-hook.sh" ~/.gemini/ph-hook.sh
|
|
12
|
+
chmod +x ~/.gemini/ph-hook.sh
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Gemini CLI Configuration
|
|
16
|
+
|
|
17
|
+
Add (or update) the `hooks` section in your `~/.gemini/settings.json` file:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"hooks": {
|
|
22
|
+
"AfterAgent": [
|
|
23
|
+
{
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "/Users/gianluca/.gemini/ph-hook.sh"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Note:** Ensure the path in the command correctly points to the symbolic link or the actual script. Using the symbolic link at `~/.gemini/ph-hook.sh` keeps the configuration portable.
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
The script is executed by Gemini CLI at the `AfterAgent` event (after each agent response).
|
|
41
|
+
1. Receives the JSON input from Gemini CLI containing the prompt and response.
|
|
42
|
+
2. Extracts the user prompt and the assistant response.
|
|
43
|
+
3. Sends the data to `ph log` in the background.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ph-hook.sh — Gemini CLI AfterAgent hook: logs the last prompt+response pair to ph
|
|
3
|
+
#
|
|
4
|
+
# Gemini CLI sends a JSON object on stdin with:
|
|
5
|
+
# {
|
|
6
|
+
# "session_id": "...",
|
|
7
|
+
# "transcript_path": "...",
|
|
8
|
+
# "cwd": "/path/to/workdir",
|
|
9
|
+
# "hook_event_name": "AfterAgent",
|
|
10
|
+
# "timestamp": "...",
|
|
11
|
+
# "prompt": "the user's prompt text",
|
|
12
|
+
# "prompt_response": "the assistant's full response text"
|
|
13
|
+
# }
|
|
14
|
+
#
|
|
15
|
+
# This script:
|
|
16
|
+
# 1. Reads the hook input JSON
|
|
17
|
+
# 2. Extracts prompt, prompt_response, and cwd
|
|
18
|
+
# 3. Pipes {"tool":"gemini","prompt":"...","response":"...","workdir":"..."} to ph log
|
|
19
|
+
# 4. Always exits 0 so Gemini CLI is never blocked
|
|
20
|
+
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
# ── Read hook input ──────────────────────────────────────────────────────────
|
|
24
|
+
HOOK_INPUT=$(cat)
|
|
25
|
+
|
|
26
|
+
PROMPT=$(printf '%s' "$HOOK_INPUT" | jq -r '.prompt // empty')
|
|
27
|
+
RESPONSE=$(printf '%s' "$HOOK_INPUT" | jq -r '.prompt_response // empty')
|
|
28
|
+
WORKDIR=$(printf '%s' "$HOOK_INPUT" | jq -r '.cwd // empty')
|
|
29
|
+
|
|
30
|
+
# Nothing to do if no prompt
|
|
31
|
+
if [[ -z "$PROMPT" ]]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Fallback workdir
|
|
36
|
+
if [[ -z "$WORKDIR" ]]; then
|
|
37
|
+
WORKDIR="$HOME"
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# ── Call ph log ───────────────────────────────────────────────────────────────
|
|
41
|
+
# Use jq to safely build the JSON payload (handles escaping automatically)
|
|
42
|
+
JSON_PAYLOAD=$(
|
|
43
|
+
jq -n \
|
|
44
|
+
--arg tool "gemini" \
|
|
45
|
+
--arg prompt "$PROMPT" \
|
|
46
|
+
--arg response "${RESPONSE:-}" \
|
|
47
|
+
--arg workdir "$WORKDIR" \
|
|
48
|
+
'{"tool":$tool,"prompt":$prompt,"response":$response,"workdir":$workdir}'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Run ph log in the background so it never blocks Gemini CLI
|
|
52
|
+
# Redirect all output to /dev/null to keep things silent
|
|
53
|
+
printf '%s' "$JSON_PAYLOAD" | ph log >/dev/null 2>&1 &
|
|
54
|
+
disown $! 2>/dev/null || true
|
|
55
|
+
|
|
56
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gi4nks/ph",
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"description": "Transparent CLI wrapper that captures and indexes AI prompts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ph": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"docs",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"hooks"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/gi4nks/ph.git"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup",
|
|
24
|
+
"dev": "tsx src/cli.ts",
|
|
25
|
+
"start": "node dist/cli.js",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"lint": "eslint src",
|
|
28
|
+
"release:patch": "standard-version --release-as patch",
|
|
29
|
+
"release:minor": "standard-version --release-as minor",
|
|
30
|
+
"release:major": "standard-version --release-as major"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@gi4nks/ink": "^1.5.0",
|
|
34
|
+
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
35
|
+
"better-sqlite3": "^12.8.0",
|
|
36
|
+
"ink": "^6.8.0",
|
|
37
|
+
"react": "^19.2.4",
|
|
38
|
+
"zod": "^4.3.6"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
42
|
+
"@types/node": "^25.5.0",
|
|
43
|
+
"@types/react": "^19.2.14",
|
|
44
|
+
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
45
|
+
"@typescript-eslint/parser": "^8.57.2",
|
|
46
|
+
"eslint": "^9.39.4",
|
|
47
|
+
"eslint-plugin-react": "^7.37.5",
|
|
48
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
49
|
+
"standard-version": "^9.5.0",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"tsx": "^4.21.0",
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"vitest": "^4.1.0"
|
|
54
|
+
}
|
|
55
|
+
}
|