promptvc 0.1.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.
@@ -0,0 +1,285 @@
1
+ #!/bin/bash
2
+ # PromptVC notification hook for Codex
3
+ # This script is called by Codex after each turn completes
4
+ # Captures prompts AND git diffs per-prompt for detailed tracking
5
+
6
+ # Check if we're in a git repository and resolve the repo root
7
+ if ! git rev-parse --git-dir > /dev/null 2>&1; then
8
+ exit 0
9
+ fi
10
+
11
+ REPO_DIR=$(git rev-parse --show-toplevel 2>/dev/null)
12
+ if [ -z "$REPO_DIR" ]; then
13
+ exit 0
14
+ fi
15
+
16
+ # Get PromptVC directory
17
+ PROMPTVC_DIR="$REPO_DIR/.promptvc"
18
+ SESSIONS_FILE="$PROMPTVC_DIR/sessions.json"
19
+ LAST_PROMPT_FILE="$PROMPTVC_DIR/last_prompt_count"
20
+ LAST_SESSION_FILE="$PROMPTVC_DIR/last_session_file"
21
+ TEMP_PROMPTS_FILE="$PROMPTVC_DIR/temp_prompts.json"
22
+ SETTINGS_FILE="$PROMPTVC_DIR/settings.json"
23
+
24
+ # Create .promptvc directory if it doesn't exist
25
+ mkdir -p "$PROMPTVC_DIR"
26
+
27
+ if [ ! -f "$SESSIONS_FILE" ]; then
28
+ echo "[]" > "$SESSIONS_FILE"
29
+ fi
30
+
31
+ # Play a notification sound when Codex finishes a response
32
+ SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
33
+ SOUND_PATH="$SCRIPT_DIR/../assets/notify.mp3"
34
+ FALLBACK_SOUND_PATH="$SCRIPT_DIR/../../../assets/notify.mp3"
35
+ play_notify_sound() {
36
+ if [ -f "$SETTINGS_FILE" ]; then
37
+ SOUND_SETTING=""
38
+ if command -v jq > /dev/null 2>&1; then
39
+ SOUND_SETTING=$(jq -r '.notifySoundEnabled // true' "$SETTINGS_FILE" 2>/dev/null)
40
+ else
41
+ SOUND_SETTING=$(sed -nE 's/.*"notifySoundEnabled"[[:space:]]*:[[:space:]]*(true|false).*/\1/p' "$SETTINGS_FILE" | head -1)
42
+ fi
43
+ if [ "$SOUND_SETTING" = "false" ]; then
44
+ return
45
+ fi
46
+ fi
47
+
48
+ if [ ! -f "$SOUND_PATH" ]; then
49
+ if [ -f "$FALLBACK_SOUND_PATH" ]; then
50
+ SOUND_PATH="$FALLBACK_SOUND_PATH"
51
+ else
52
+ return
53
+ fi
54
+ fi
55
+
56
+ if command -v afplay > /dev/null 2>&1; then
57
+ afplay "$SOUND_PATH" > /dev/null 2>&1 &
58
+ elif command -v paplay > /dev/null 2>&1; then
59
+ paplay "$SOUND_PATH" > /dev/null 2>&1 &
60
+ elif command -v aplay > /dev/null 2>&1; then
61
+ aplay "$SOUND_PATH" > /dev/null 2>&1 &
62
+ elif command -v play > /dev/null 2>&1; then
63
+ play "$SOUND_PATH" > /dev/null 2>&1 &
64
+ fi
65
+ }
66
+ trap 'play_notify_sound' EXIT
67
+
68
+ # Find the latest codex session file
69
+ LATEST_SESSION=$(find "$HOME/.codex/sessions" -name "rollout-*.jsonl" -type f -print0 2>/dev/null | \
70
+ xargs -0 ls -t 2>/dev/null | head -1)
71
+
72
+ if [ ! -f "$LATEST_SESSION" ]; then
73
+ exit 0
74
+ fi
75
+
76
+ SESSION_ID=$(basename "$LATEST_SESSION" .jsonl)
77
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
78
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
79
+
80
+ # Reset prompt counter when a new Codex session file appears
81
+ PREV_SESSION=""
82
+ if [ -f "$LAST_SESSION_FILE" ]; then
83
+ PREV_SESSION=$(cat "$LAST_SESSION_FILE")
84
+ fi
85
+ NEW_SESSION="false"
86
+ if [ "$LATEST_SESSION" != "$PREV_SESSION" ]; then
87
+ echo "0" > "$LAST_PROMPT_FILE"
88
+ echo "$LATEST_SESSION" > "$LAST_SESSION_FILE"
89
+ NEW_SESSION="true"
90
+ fi
91
+
92
+ # Ensure jq is available
93
+ if ! command -v jq > /dev/null 2>&1; then
94
+ exit 0
95
+ fi
96
+
97
+ SESSIONS_JSON=$(cat "$SESSIONS_FILE" 2>/dev/null || echo "[]")
98
+ if ! echo "$SESSIONS_JSON" | jq -e . > /dev/null 2>&1; then
99
+ SESSIONS_JSON="[]"
100
+ fi
101
+
102
+ if [ "$NEW_SESSION" = "true" ] && [ -n "$PREV_SESSION" ]; then
103
+ PREV_SESSION_ID=$(basename "$PREV_SESSION" .jsonl)
104
+ if [ -n "$PREV_SESSION_ID" ]; then
105
+ SESSIONS_JSON=$(echo "$SESSIONS_JSON" | jq \
106
+ --arg prevId "$PREV_SESSION_ID" \
107
+ --arg endedAt "$TIMESTAMP" \
108
+ 'map(if .id == $prevId then .inProgress = false | .endedAt = $endedAt else . end)')
109
+ fi
110
+ fi
111
+
112
+ # Extract all user prompts as a JSON array
113
+ # This preserves multi-line prompts as single entries
114
+ cat "$LATEST_SESSION" | \
115
+ jq -R -s 'split("\n") | map(fromjson? | select(.type == "response_item" and .payload.role == "user") | .payload.content[0].text) | map(select(. != null))' \
116
+ > "$TEMP_PROMPTS_FILE" 2>/dev/null
117
+
118
+ if [ ! -f "$TEMP_PROMPTS_FILE" ] || [ ! -s "$TEMP_PROMPTS_FILE" ]; then
119
+ exit 0
120
+ fi
121
+
122
+ # Filter out system prompts using jq (checking entire message content)
123
+ # Remove prompts that contain system instruction markers
124
+ FILTERED_PROMPTS=$(cat "$TEMP_PROMPTS_FILE" | \
125
+ jq 'map(select(
126
+ (. | startswith("# AGENTS.md") | not) and
127
+ (. | startswith("<INSTRUCTIONS>") | not) and
128
+ (. | startswith("<environment_context>") | not) and
129
+ (. | contains("# AGENTS.md instructions") | not) and
130
+ (. | contains("<INSTRUCTIONS>") | not) and
131
+ (. | contains("## Skills") | not) and
132
+ (. | contains("These skills are discovered at startup") | not)
133
+ ))' 2>/dev/null)
134
+
135
+ # Count current prompts
136
+ CURRENT_PROMPT_COUNT=$(echo "$FILTERED_PROMPTS" | jq 'length' 2>/dev/null || echo "0")
137
+
138
+ if [ "$CURRENT_PROMPT_COUNT" = "0" ]; then
139
+ rm -f "$TEMP_PROMPTS_FILE"
140
+ exit 0
141
+ fi
142
+
143
+ # Get the last processed prompt count
144
+ LAST_PROMPT_COUNT=0
145
+ if [ -f "$LAST_PROMPT_FILE" ]; then
146
+ LAST_PROMPT_COUNT=$(cat "$LAST_PROMPT_FILE")
147
+ fi
148
+
149
+ # If no new prompts, exit
150
+ if [ "$CURRENT_PROMPT_COUNT" -le "$LAST_PROMPT_COUNT" ]; then
151
+ rm -f "$TEMP_PROMPTS_FILE"
152
+ exit 0
153
+ fi
154
+
155
+ # Get the new prompts (starting from LAST_PROMPT_COUNT)
156
+ NEW_PROMPTS=$(echo "$FILTERED_PROMPTS" | jq ".[$LAST_PROMPT_COUNT:]" 2>/dev/null)
157
+
158
+ # Capture current git state
159
+ GIT_HASH=$(git rev-parse HEAD 2>/dev/null || echo "")
160
+ GIT_DIFF=$(git diff 2>/dev/null || echo "")
161
+ CHANGED_FILES_RAW=$(git diff --name-only 2>/dev/null || echo "")
162
+
163
+ # Build files array as JSON
164
+ if [ -z "$CHANGED_FILES_RAW" ]; then
165
+ FILES_ARRAY="[]"
166
+ else
167
+ FILES_ARRAY=$(echo "$CHANGED_FILES_RAW" | jq -R -s 'split("\n") | map(select(length > 0))')
168
+ fi
169
+
170
+ # Escape diff for JSON
171
+ ESCAPED_DIFF=$(echo "$GIT_DIFF" | jq -R -s '.')
172
+
173
+ # Process each new prompt
174
+ PROMPT_COUNT=$(echo "$NEW_PROMPTS" | jq 'length' 2>/dev/null || echo "0")
175
+
176
+ if [ "$PROMPT_COUNT" = "0" ]; then
177
+ rm -f "$TEMP_PROMPTS_FILE"
178
+ exit 0
179
+ fi
180
+
181
+ # Build array of new prompt entries
182
+ NEW_ENTRIES="[]"
183
+ for ((i=0; i<$PROMPT_COUNT; i++)); do
184
+ PROMPT=$(echo "$NEW_PROMPTS" | jq -r ".[$i]" 2>/dev/null)
185
+
186
+ if [ -z "$PROMPT" ] || [ "$PROMPT" = "null" ]; then
187
+ continue
188
+ fi
189
+
190
+ # Escape prompt for JSON
191
+ ESCAPED_PROMPT=$(echo "$PROMPT" | jq -R -s '.')
192
+
193
+ # Create prompt entry
194
+ PROMPT_ENTRY=$(cat <<EOF
195
+ {
196
+ "prompt": $ESCAPED_PROMPT,
197
+ "timestamp": "$TIMESTAMP",
198
+ "hash": "$GIT_HASH",
199
+ "files": $FILES_ARRAY,
200
+ "diff": $ESCAPED_DIFF
201
+ }
202
+ EOF
203
+ )
204
+
205
+ # Add to new entries array
206
+ NEW_ENTRIES=$(echo "$NEW_ENTRIES" | jq ". + [$PROMPT_ENTRY]" 2>/dev/null)
207
+ done
208
+
209
+ LATEST_PROMPT=$(echo "$NEW_PROMPTS" | jq -r '.[-1]' 2>/dev/null)
210
+ if [ -z "$LATEST_PROMPT" ] || [ "$LATEST_PROMPT" = "null" ]; then
211
+ LATEST_PROMPT=""
212
+ fi
213
+
214
+ # Update sessions.json with the latest prompt changes
215
+ if [ "$NEW_ENTRIES" != "[]" ]; then
216
+ UPDATED_SESSIONS=$(echo "$SESSIONS_JSON" | jq \
217
+ --arg id "$SESSION_ID" \
218
+ --arg repoRoot "$REPO_DIR" \
219
+ --arg branch "$BRANCH" \
220
+ --arg timestamp "$TIMESTAMP" \
221
+ --arg prompt "$LATEST_PROMPT" \
222
+ --arg hash "$GIT_HASH" \
223
+ --argjson files "$FILES_ARRAY" \
224
+ --argjson diff "$ESCAPED_DIFF" \
225
+ --argjson newEntries "$NEW_ENTRIES" \
226
+ '
227
+ def merge_files(existing; incoming):
228
+ reduce incoming[] as $item (existing; if index($item) then . else . + [$item] end);
229
+ def prompt_count(entries):
230
+ entries | length;
231
+ def response_snippet(entries):
232
+ "Interactive session: " + (prompt_count(entries) | tostring) + " prompt" + (if prompt_count(entries) != 1 then "s" else "" end);
233
+ def update_session(session):
234
+ session
235
+ | .provider = "codex"
236
+ | .repoRoot = $repoRoot
237
+ | .branch = $branch
238
+ | .prompt = $prompt
239
+ | .diff = $diff
240
+ | .mode = "interactive"
241
+ | .autoTagged = true
242
+ | .inProgress = true
243
+ | .updatedAt = $timestamp
244
+ | .files = merge_files((.files // []); $files)
245
+ | .perPromptChanges = ((.perPromptChanges // []) + $newEntries)
246
+ | .responseSnippet = response_snippet(.perPromptChanges);
247
+ if map(.id == $id) | any then
248
+ map(if .id == $id then
249
+ update_session(.)
250
+ | if (.createdAt // "") == "" then .createdAt = $timestamp else . end
251
+ | if (.preHash // "") == "" then .preHash = $hash else . end
252
+ | .postHash = (.postHash // null)
253
+ else . end)
254
+ else
255
+ [ {
256
+ id: $id,
257
+ provider: "codex",
258
+ repoRoot: $repoRoot,
259
+ branch: $branch,
260
+ preHash: $hash,
261
+ postHash: null,
262
+ prompt: $prompt,
263
+ responseSnippet: response_snippet($newEntries),
264
+ files: $files,
265
+ diff: $diff,
266
+ createdAt: $timestamp,
267
+ updatedAt: $timestamp,
268
+ mode: "interactive",
269
+ autoTagged: true,
270
+ inProgress: true,
271
+ perPromptChanges: $newEntries
272
+ } ] + .
273
+ end
274
+ ')
275
+
276
+ echo "$UPDATED_SESSIONS" > "$SESSIONS_FILE"
277
+
278
+ # Update last prompt count
279
+ echo "$CURRENT_PROMPT_COUNT" > "$LAST_PROMPT_FILE"
280
+ fi
281
+
282
+ # Clean up
283
+ rm -f "$TEMP_PROMPTS_FILE"
284
+
285
+ exit 0
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "promptvc",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to wrap AI assistants and log prompts/diffs",
5
+ "homepage": "https://v0-prompt-version-control-pi.vercel.app/",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/sehmim/PromptVC.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/sehmim/PromptVC/issues"
12
+ },
13
+ "main": "dist/index.js",
14
+ "bin": {
15
+ "promptvc": "bin/promptvc",
16
+ "promptvc-codex": "bin/promptvc-codex"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "dist",
21
+ "hooks",
22
+ "assets"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "clean": "rm -rf dist",
27
+ "dev": "tsc --watch",
28
+ "prepublishOnly": "npm run build",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "dependencies": {
32
+ "@promptvc/types": "workspace:*",
33
+ "kleur": "^4.1.5",
34
+ "commander": "^11.1.0",
35
+ "simple-git": "^3.22.0",
36
+ "uuid": "^9.0.1"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.10.6",
40
+ "@types/uuid": "^9.0.7",
41
+ "typescript": "^5.3.3"
42
+ }
43
+ }