@gi4nks/ph 1.3.2 → 1.5.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 +40 -5
- package/README.md +172 -91
- package/dist/cli.js +3890 -1952
- package/dist/cli.js.map +1 -1
- package/docs/hooks.md +33 -71
- package/docs/ph-manual.md +246 -8
- package/hooks/opencode/install.sh +45 -0
- package/hooks/opencode/ph-plugin.ts +101 -0
- package/package.json +36 -5
package/docs/hooks.md
CHANGED
|
@@ -1,83 +1,45 @@
|
|
|
1
|
-
# ph
|
|
1
|
+
# ph Hook System — Status Report
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Completed Hooks
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
└───────────────────────────────────────────────────────────┴────────────────────────────┴────────┘
|
|
5
|
+
| Tool | Type | File | Status |
|
|
6
|
+
|------|------|------|--------|
|
|
7
|
+
| Claude Code | Shell script (Stop hook) | `hooks/claude/ph-hook.sh` | ✅ |
|
|
8
|
+
| Gemini CLI | Shell script (AfterAgent hook) | `hooks/gemini/ph-hook.sh` | ✅ |
|
|
9
|
+
| OpenCode | JS/TS Plugin | `hooks/opencode/ph-plugin.ts` | ✅ |
|
|
32
10
|
|
|
33
|
-
|
|
11
|
+
## How Hooks Work
|
|
34
12
|
|
|
35
|
-
|
|
36
|
-
- Gemini CLI skill/hook: similar integration for the Gemini CLI
|
|
13
|
+
Each supported AI CLI tool exposes a native hook mechanism:
|
|
37
14
|
|
|
38
|
-
|
|
15
|
+
- **Claude Code** — `Stop` hook, triggered after each agent session completes.
|
|
16
|
+
Reads the JSONL transcript, extracts last user prompt + assistant response, pipes to `ph log`.
|
|
39
17
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
18
|
+
- **Gemini CLI** — `AfterAgent` hook, triggered after each agent turn.
|
|
19
|
+
Receives JSON with `prompt` and `prompt_response` fields directly, pipes to `ph log`.
|
|
46
20
|
|
|
47
|
-
|
|
21
|
+
- **OpenCode** — Plugin system with `chat.message` + `experimental.text.complete` hooks.
|
|
22
|
+
Intercepts messages, pairs user prompts with streamed assistant responses, pipes to `ph log`.
|
|
48
23
|
|
|
49
|
-
|
|
50
|
-
|
|
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)
|
|
24
|
+
All hooks run in the background (`& + disown` for shell scripts; Promise fire-and-forget for the plugin).
|
|
25
|
+
Never block the AI tool from exiting.
|
|
52
26
|
|
|
53
|
-
|
|
27
|
+
## Data Flow
|
|
54
28
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
29
|
+
```
|
|
30
|
+
AI CLI Tool
|
|
31
|
+
↓ (automatic hook after each exchange)
|
|
32
|
+
ph log (stdin JSON)
|
|
33
|
+
↓
|
|
34
|
+
SQLite (~/.prompt_history.db)
|
|
35
|
+
↓
|
|
36
|
+
background analysis (Ollama)
|
|
37
|
+
↓
|
|
38
|
+
metadata: project, language, role, tags, relevance
|
|
39
|
+
↓
|
|
40
|
+
ph browse / ph search / ph sessions
|
|
41
|
+
```
|
|
66
42
|
|
|
67
|
-
|
|
43
|
+
## Installation
|
|
68
44
|
|
|
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.
|
|
45
|
+
See `docs/ph-manual.md` for detailed installation instructions per tool.
|
package/docs/ph-manual.md
CHANGED
|
@@ -165,16 +165,94 @@ ln -sf /path/to/ph/hooks/gemini/ph-hook.sh ~/.gemini/ph-hook.sh
|
|
|
165
165
|
|
|
166
166
|
---
|
|
167
167
|
|
|
168
|
+
### OpenCode Hook
|
|
169
|
+
|
|
170
|
+
OpenCode uses a JavaScript/TypeScript **plugin** system (rather than shell scripts). The `ph` integration is a plugin that hooks into OpenCode's message lifecycle to capture prompts and responses.
|
|
171
|
+
|
|
172
|
+
#### How the plugin captures data
|
|
173
|
+
|
|
174
|
+
The plugin registers two hooks inside OpenCode's runtime:
|
|
175
|
+
|
|
176
|
+
| Hook | Purpose |
|
|
177
|
+
|------|---------|
|
|
178
|
+
| `chat.message` | Intercepts every message in the conversation. User messages become prompts; assistant messages become response snapshots. |
|
|
179
|
+
| `experimental.text.complete` | Fires after streaming completes. Replaces the snapshot with the full, final response text. |
|
|
180
|
+
|
|
181
|
+
**Data flow:**
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
User types prompt
|
|
185
|
+
→ chat.message fires (role: user)
|
|
186
|
+
→ plugin stores the prompt text + metadata (agent, model, workdir)
|
|
187
|
+
|
|
188
|
+
Assistant streams response
|
|
189
|
+
→ experimental.text.complete fires for each text part
|
|
190
|
+
→ plugin accumulates the complete response text
|
|
191
|
+
|
|
192
|
+
Next user message or session end
|
|
193
|
+
→ plugin pairs prompt + accumulated response
|
|
194
|
+
→ pipes {"tool":"opencode","prompt":"...","response":"..."} to ph log
|
|
195
|
+
→ runs in background — never blocks OpenCode
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Hook installation — OpenCode
|
|
199
|
+
|
|
200
|
+
OpenCode loads plugins from two directories:
|
|
201
|
+
- `~/.config/opencode/plugins/` — global (all projects)
|
|
202
|
+
- `.opencode/plugins/` — per-project
|
|
203
|
+
|
|
204
|
+
**Option A: Quick install (global)**
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Link the plugin into OpenCode's global plugin directory
|
|
208
|
+
mkdir -p ~/.config/opencode/plugins
|
|
209
|
+
ln -sf /path/to/ph/hooks/opencode/ph-plugin.ts ~/.config/opencode/plugins/ph-plugin.ts
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or use the install script:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
./hooks/opencode/install.sh # global (default)
|
|
216
|
+
./hooks/opencode/install.sh --project # current project only
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Option B: Manual copy**
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
cp hooks/opencode/ph-plugin.ts ~/.config/opencode/plugins/
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The plugin is automatically loaded on the next OpenCode startup. No config changes needed — Bun handles TypeScript natively.
|
|
226
|
+
|
|
227
|
+
**Verify:**
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# Start OpenCode — the plugin loads silently
|
|
231
|
+
opencode
|
|
232
|
+
|
|
233
|
+
# Check that prompts are being captured
|
|
234
|
+
ph search --tool opencode --limit 3
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Uninstall:**
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
rm ~/.config/opencode/plugins/ph-plugin.ts
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
168
245
|
### Hook Comparison
|
|
169
246
|
|
|
170
|
-
| Aspect | Claude Code | Gemini CLI |
|
|
171
|
-
|
|
172
|
-
| Hook
|
|
173
|
-
| Data delivery | `transcript_path` (JSONL
|
|
174
|
-
| Prompt extraction | Parse JSONL with `jq` slurp | `jq .prompt`
|
|
175
|
-
| Response extraction | Last assistant text block | `jq .prompt_response`
|
|
176
|
-
| Async support | Yes (`async: true`
|
|
177
|
-
| Blocks AI exit? | Never
|
|
247
|
+
| Aspect | Claude Code | Gemini CLI | OpenCode |
|
|
248
|
+
|--------|------------|------------|----------|
|
|
249
|
+
| Hook mechanism | `Stop` shell script | `AfterAgent` shell script | JS/TS plugin |
|
|
250
|
+
| Data delivery | `transcript_path` (JSONL) | Direct JSON on stdin | Event hooks (`chat.message`, `experimental.text.complete`) |
|
|
251
|
+
| Prompt extraction | Parse JSONL with `jq` slurp | `jq .prompt` | `message.role === 'user'` parts |
|
|
252
|
+
| Response extraction | Last assistant text block | `jq .prompt_response` | `experimental.text.complete` accumulation |
|
|
253
|
+
| Async support | Yes (`async: true`) | Yes (`& + disown`) | Yes (Promise fire-and-forget) |
|
|
254
|
+
| Blocks AI exit? | Never | Never | Never |
|
|
255
|
+
| Install method | Symlink + settings.json | Symlink + settings.json | Copy plugin file to `plugins/` dir |
|
|
178
256
|
|
|
179
257
|
---
|
|
180
258
|
|
|
@@ -278,6 +356,166 @@ ph treats your prompt history as a first-class artifact: searchable, annotated,
|
|
|
278
356
|
|
|
279
357
|
---
|
|
280
358
|
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## 7. Remote Server Setup
|
|
362
|
+
|
|
363
|
+
ph can act as a central sync hub for prompt history across multiple machines.
|
|
364
|
+
|
|
365
|
+
### Server Installation
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# Install globally
|
|
369
|
+
sudo npm install -g @gi4nks/ph
|
|
370
|
+
|
|
371
|
+
# Verify
|
|
372
|
+
ph --help
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Ubuntu (systemd service)
|
|
376
|
+
|
|
377
|
+
Create `/etc/systemd/system/ph.service`:
|
|
378
|
+
|
|
379
|
+
```ini
|
|
380
|
+
[Unit]
|
|
381
|
+
Description=ph remote sync server
|
|
382
|
+
After=network.target
|
|
383
|
+
|
|
384
|
+
[Service]
|
|
385
|
+
Type=simple
|
|
386
|
+
ExecStart=<path-to-ph> server --port 3001 --host 0.0.0.0
|
|
387
|
+
Restart=always
|
|
388
|
+
RestartSec=5
|
|
389
|
+
User=<your-user>
|
|
390
|
+
Environment=NODE_ENV=production
|
|
391
|
+
StandardOutput=append:/var/log/ph-server.log
|
|
392
|
+
StandardError=append:/var/log/ph-server.log
|
|
393
|
+
|
|
394
|
+
[Install]
|
|
395
|
+
WantedBy=multi-user.target
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Replace `<path-to-ph>` with the output of `which ph` on the server, and `<your-user>` with the user running the service.
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
sudo systemctl daemon-reload
|
|
402
|
+
sudo systemctl enable --now ph.service
|
|
403
|
+
sudo systemctl status ph.service
|
|
404
|
+
sudo ufw allow 3001/tcp # if firewall is enabled
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### macOS (launchd)
|
|
408
|
+
|
|
409
|
+
Create `~/Library/LaunchAgents/com.gi4nks.ph.plist`:
|
|
410
|
+
|
|
411
|
+
```xml
|
|
412
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
413
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
414
|
+
<plist version="1.0">
|
|
415
|
+
<dict>
|
|
416
|
+
<key>Label</key>
|
|
417
|
+
<string>com.gi4nks.ph</string>
|
|
418
|
+
<key>ProgramArguments</key>
|
|
419
|
+
<array>
|
|
420
|
+
<string>/usr/local/bin/ph</string>
|
|
421
|
+
<string>server</string>
|
|
422
|
+
<string>--port</string>
|
|
423
|
+
<string>3001</string>
|
|
424
|
+
<string>--host</string>
|
|
425
|
+
<string>0.0.0.0</string>
|
|
426
|
+
</array>
|
|
427
|
+
<key>RunAtLoad</key>
|
|
428
|
+
<true/>
|
|
429
|
+
<key>KeepAlive</key>
|
|
430
|
+
<true/>
|
|
431
|
+
<key>StandardOutPath</key>
|
|
432
|
+
<string>/tmp/ph-server.log</string>
|
|
433
|
+
<key>StandardErrorPath</key>
|
|
434
|
+
<string>/tmp/ph-server.err</string>
|
|
435
|
+
</dict>
|
|
436
|
+
</plist>
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
launchctl load ~/Library/LaunchAgents/com.gi4nks.ph.plist
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Client Setup
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
# Configure remote server URL
|
|
447
|
+
ph config set remote-url http://<server-ip>:3001
|
|
448
|
+
# Or via env var (takes precedence)
|
|
449
|
+
export PH_REMOTE_URL=http://<server-ip>:3001
|
|
450
|
+
|
|
451
|
+
# Sync commands
|
|
452
|
+
ph remote push # send unsynced local prompts to server
|
|
453
|
+
ph remote pull # fetch remote prompts since last pull
|
|
454
|
+
ph remote status # show sync state
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
After `ph remote-url` is configured, every `ph log` automatically does a background push to the server (fire-and-forget, never blocks).
|
|
458
|
+
|
|
459
|
+
### Logs
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
# systemd
|
|
463
|
+
sudo journalctl -u ph.service -f
|
|
464
|
+
|
|
465
|
+
# launchd
|
|
466
|
+
cat /tmp/ph-server.log
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## 8. Release Process
|
|
472
|
+
|
|
473
|
+
Releases are fully automated via **semantic-release** using conventional commits.
|
|
474
|
+
|
|
475
|
+
### How it works
|
|
476
|
+
|
|
477
|
+
1. All commits must follow the [Conventional Commits](https://www.conventionalcommits.org/) format:
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
feat: add new feature → minor release (1.x.0)
|
|
481
|
+
fix: resolve bug → patch release (1.0.x)
|
|
482
|
+
chore: bump deps → no release
|
|
483
|
+
BREAKING CHANGE: ... → major release (x.0.0)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
2. Push to `main` triggers GitHub Actions workflow `.github/workflows/release.yml`
|
|
487
|
+
|
|
488
|
+
3. The workflow runs:
|
|
489
|
+
- `actions/checkout@v4` with full git history (`fetch-depth: 0`)
|
|
490
|
+
- `actions/setup-node@v4` with Node 22
|
|
491
|
+
- `npm ci` — clean install
|
|
492
|
+
- `npm run build` — tsup bundle
|
|
493
|
+
- `npx semantic-release` — analyzes commits, bumps version, updates CHANGELOG.md, creates git tag, publishes to npm
|
|
494
|
+
|
|
495
|
+
### npm publish details
|
|
496
|
+
|
|
497
|
+
- **OIDC Trusted Publishing**: Package is signed with provenance attestation via GitHub Actions OIDC token
|
|
498
|
+
- **Publisher identity**: `GitHub Actions <npm-oidc-no-reply@github.com>`
|
|
499
|
+
- **Registry**: `https://registry.npmjs.org/`
|
|
500
|
+
- **Access**: public
|
|
501
|
+
|
|
502
|
+
### Verify provenance
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
npm audit signatures
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Manual release (emergency)
|
|
509
|
+
|
|
510
|
+
Create a tag manually and push:
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
git tag v1.2.3
|
|
514
|
+
git push origin v1.2.3
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
281
519
|
## Repository
|
|
282
520
|
|
|
283
521
|
[https://github.com/gi4nks/ph](https://github.com/gi4nks/ph)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install.sh — Install the ph OpenCode plugin
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./install.sh [--global|--project]
|
|
5
|
+
# --global Install in ~/.config/opencode/plugins/ (default)
|
|
6
|
+
# --project Install in .opencode/plugins/ (current directory)
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
PLUGIN_SOURCE="$(cd "$(dirname "$0")" && pwd)/ph-plugin.ts"
|
|
11
|
+
MODE="${1:---global}"
|
|
12
|
+
|
|
13
|
+
case "$MODE" in
|
|
14
|
+
--global)
|
|
15
|
+
TARGET_DIR="$HOME/.config/opencode/plugins"
|
|
16
|
+
;;
|
|
17
|
+
--project)
|
|
18
|
+
TARGET_DIR=".opencode/plugins"
|
|
19
|
+
mkdir -p "$TARGET_DIR"
|
|
20
|
+
TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
|
|
21
|
+
;;
|
|
22
|
+
*)
|
|
23
|
+
echo "Usage: $0 [--global|--project]"
|
|
24
|
+
exit 1
|
|
25
|
+
;;
|
|
26
|
+
esac
|
|
27
|
+
|
|
28
|
+
mkdir -p "$TARGET_DIR"
|
|
29
|
+
|
|
30
|
+
TARGET_FILE="$TARGET_DIR/ph-plugin.ts"
|
|
31
|
+
|
|
32
|
+
if [ -f "$TARGET_FILE" ]; then
|
|
33
|
+
echo "Updating existing plugin at $TARGET_FILE"
|
|
34
|
+
else
|
|
35
|
+
echo "Installing plugin to $TARGET_FILE"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
cp "$PLUGIN_SOURCE" "$TARGET_FILE"
|
|
39
|
+
echo "✅ ph OpenCode plugin installed at $TARGET_FILE"
|
|
40
|
+
|
|
41
|
+
echo ""
|
|
42
|
+
echo "Plugin will be loaded automatically on next OpenCode startup."
|
|
43
|
+
echo "To verify: opencode config will show it in the plugin list."
|
|
44
|
+
echo ""
|
|
45
|
+
echo "To uninstall: rm $TARGET_FILE"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
interface PendingPrompt {
|
|
4
|
+
text: string;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
agent?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Per-session state for pairing user prompts with assistant responses */
|
|
11
|
+
const sessions = new Map<string, {
|
|
12
|
+
prompt: PendingPrompt | null;
|
|
13
|
+
response: string;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
function textFromParts(parts: { type?: string; text?: string }[]): string {
|
|
17
|
+
return parts
|
|
18
|
+
.filter(p => p.type === 'text')
|
|
19
|
+
.map(p => p.text || '')
|
|
20
|
+
.join('')
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function callPhLog(
|
|
25
|
+
$: any,
|
|
26
|
+
prompt: PendingPrompt,
|
|
27
|
+
response: string,
|
|
28
|
+
workdir: string,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
if (!prompt.text || !response) return;
|
|
31
|
+
|
|
32
|
+
const payload: Record<string, unknown> = {
|
|
33
|
+
tool: 'opencode',
|
|
34
|
+
prompt: prompt.text,
|
|
35
|
+
response,
|
|
36
|
+
workdir,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const meta: Record<string, unknown> = { source: 'plugin' };
|
|
40
|
+
if (prompt.agent) meta.agent = prompt.agent;
|
|
41
|
+
if (prompt.model) meta.model = prompt.model;
|
|
42
|
+
payload.metadata = JSON.stringify(meta);
|
|
43
|
+
|
|
44
|
+
const json = JSON.stringify(payload);
|
|
45
|
+
const _ = $`echo ${json} | ph log > /dev/null 2>&1`;
|
|
46
|
+
_.then(() => {}).catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const PhOpenCodePlugin: Plugin = async ({ directory, $ }) => {
|
|
50
|
+
return {
|
|
51
|
+
"chat.message": async (input, output) => {
|
|
52
|
+
const { sessionID, agent, model } = input;
|
|
53
|
+
const { message, parts } = output;
|
|
54
|
+
const role = (message as Record<string, unknown>).role;
|
|
55
|
+
|
|
56
|
+
if (role === 'user') {
|
|
57
|
+
const existing = sessions.get(sessionID);
|
|
58
|
+
const prompt = existing?.prompt;
|
|
59
|
+
const response = existing?.response;
|
|
60
|
+
|
|
61
|
+
// Log previous turn if we have both parts
|
|
62
|
+
if (prompt && response) {
|
|
63
|
+
await callPhLog($, prompt, response, directory);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Store new prompt, reset response
|
|
67
|
+
const text = textFromParts(parts);
|
|
68
|
+
if (text) {
|
|
69
|
+
sessions.set(sessionID, {
|
|
70
|
+
prompt: {
|
|
71
|
+
text,
|
|
72
|
+
timestamp: new Date().toISOString(),
|
|
73
|
+
agent,
|
|
74
|
+
model: model ? `${model.providerID}/${model.modelID}` : undefined,
|
|
75
|
+
},
|
|
76
|
+
response: '',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
} else if (role === 'assistant') {
|
|
80
|
+
const existing = sessions.get(sessionID);
|
|
81
|
+
if (!existing?.prompt) return;
|
|
82
|
+
|
|
83
|
+
// Snapshot whatever text is available (non-streaming or initial frame)
|
|
84
|
+
const text = textFromParts(parts);
|
|
85
|
+
if (text) {
|
|
86
|
+
sessions.set(sessionID, { ...existing, response: text });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
"experimental.text.complete": async (input, output) => {
|
|
92
|
+
const session = sessions.get(input.sessionID);
|
|
93
|
+
if (!session?.prompt) return;
|
|
94
|
+
|
|
95
|
+
// Replace with complete text (handles streaming: this fires after streaming ends)
|
|
96
|
+
if (output.text) {
|
|
97
|
+
sessions.set(input.sessionID, { ...session, response: output.text });
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gi4nks/ph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Transparent CLI wrapper that captures and indexes AI prompts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,25 +19,55 @@
|
|
|
19
19
|
"type": "git",
|
|
20
20
|
"url": "git+https://github.com/gi4nks/ph.git"
|
|
21
21
|
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"release": {
|
|
28
|
+
"branches": [
|
|
29
|
+
"main"
|
|
30
|
+
],
|
|
31
|
+
"plugins": [
|
|
32
|
+
"@semantic-release/commit-analyzer",
|
|
33
|
+
"@semantic-release/release-notes-generator",
|
|
34
|
+
"@semantic-release/changelog",
|
|
35
|
+
"@semantic-release/npm",
|
|
36
|
+
"@semantic-release/github",
|
|
37
|
+
[
|
|
38
|
+
"@semantic-release/git",
|
|
39
|
+
{
|
|
40
|
+
"assets": [
|
|
41
|
+
"package.json",
|
|
42
|
+
"CHANGELOG.md"
|
|
43
|
+
],
|
|
44
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
]
|
|
48
|
+
},
|
|
22
49
|
"scripts": {
|
|
23
50
|
"build": "tsup",
|
|
24
51
|
"dev": "tsx src/cli.ts",
|
|
25
52
|
"start": "node dist/cli.js",
|
|
26
53
|
"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"
|
|
54
|
+
"lint": "eslint src"
|
|
31
55
|
},
|
|
32
56
|
"dependencies": {
|
|
33
57
|
"@gi4nks/ink": "^1.5.0",
|
|
34
58
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
59
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
60
|
"better-sqlite3": "^12.8.0",
|
|
36
61
|
"ink": "^6.8.0",
|
|
37
62
|
"react": "^19.2.4",
|
|
63
|
+
"sqlite-vec": "^0.1.9",
|
|
38
64
|
"zod": "^4.3.6"
|
|
39
65
|
},
|
|
40
66
|
"devDependencies": {
|
|
67
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
68
|
+
"@semantic-release/git": "^10.0.1",
|
|
69
|
+
"@semantic-release/github": "^12.0.8",
|
|
70
|
+
"@semantic-release/npm": "^13.1.5",
|
|
41
71
|
"@types/better-sqlite3": "^7.6.13",
|
|
42
72
|
"@types/node": "^25.5.0",
|
|
43
73
|
"@types/react": "^19.2.14",
|
|
@@ -46,6 +76,7 @@
|
|
|
46
76
|
"eslint": "^9.39.4",
|
|
47
77
|
"eslint-plugin-react": "^7.37.5",
|
|
48
78
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
79
|
+
"semantic-release": "^25.0.3",
|
|
49
80
|
"standard-version": "^9.5.0",
|
|
50
81
|
"tsup": "^8.5.1",
|
|
51
82
|
"tsx": "^4.21.0",
|