@gi4nks/ph 1.3.2 → 1.4.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/docs/hooks.md CHANGED
@@ -1,83 +1,45 @@
1
- # ph Implementation Status — Complete Report
1
+ # ph Hook System — Status Report
2
2
 
3
- ✅ Completed
3
+ ## Completed Hooks
4
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
- └───────────────────────────────────────────────────────────┴────────────────────────────┴────────┘
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
- 🔄 In Progress (background agent)
11
+ ## How Hooks Work
34
12
 
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
13
+ Each supported AI CLI tool exposes a native hook mechanism:
37
14
 
38
- 📋 Hook Architecture (being implemented)
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
- 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
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
- ⏳ Still To Do (after hooks)
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
- - 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)
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
- Final Data Flow
27
+ ## Data Flow
54
28
 
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
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
- 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.
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 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`) |
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
 
@@ -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.2",
3
+ "version": "1.4.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",