patchcord 0.5.18 → 0.5.19

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/bin/patchcord.mjs CHANGED
@@ -85,7 +85,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
85
85
  const flags = cmd?.startsWith("--") ? process.argv.slice(2) : process.argv.slice(3);
86
86
  const fullStatusline = flags.includes("--full");
87
87
  let wasPluginInstalled = false;
88
- const { readFileSync, writeFileSync, unlinkSync, rmSync } = await import("fs");
88
+ const { readFileSync, writeFileSync, unlinkSync, rmSync, chmodSync, copyFileSync } = await import("fs");
89
89
 
90
90
  function safeReadJson(filePath) {
91
91
  try {
@@ -280,15 +280,44 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
280
280
  if (geminiChanged) globalChanges.push("Gemini CLI skills + commands installed");
281
281
  }
282
282
 
283
- // Codex CLI — clean up old apps.patchcord setting (it blocks the plugin)
283
+ // Codex CLI — clean up old settings and install stop hook
284
284
  const codexConfig = join(HOME, ".codex", "config.toml");
285
285
  if (existsSync(codexConfig)) {
286
- const content = readFileSync(codexConfig, "utf-8");
287
- if (content.includes("[apps.patchcord]")) {
288
- const cleaned = content.replace(/\[apps\.patchcord\]\n(?:(?!\[)[^\n]*\n?)*/g, "").replace(/\n{3,}/g, "\n\n").trim();
289
- writeFileSync(codexConfig, cleaned + "\n");
286
+ let globalCodexContent = readFileSync(codexConfig, "utf-8");
287
+
288
+ // Remove old apps.patchcord setting (it blocks the plugin)
289
+ if (globalCodexContent.includes("[apps.patchcord]")) {
290
+ globalCodexContent = globalCodexContent.replace(/\[apps\.patchcord\]\n(?:(?!\[)[^\n]*\n?)*/g, "").replace(/\n{3,}/g, "\n\n").trim();
290
291
  globalChanges.push("Removed old apps.patchcord setting");
291
292
  }
293
+
294
+ // Install/update stop hook — fires after each Codex turn to check inbox
295
+ const hookScriptSrc = join(pluginRoot, "scripts", "codex-stop-hook.sh");
296
+ const hookScriptDest = join(HOME, ".codex", "patchcord-stop-hook.sh");
297
+ if (existsSync(hookScriptSrc)) {
298
+ const hookAlreadyExisted = existsSync(hookScriptDest);
299
+ copyFileSync(hookScriptSrc, hookScriptDest);
300
+ chmodSync(hookScriptDest, 0o755);
301
+
302
+ // Enable codex_hooks feature flag if not already set
303
+ if (!globalCodexContent.includes("codex_hooks")) {
304
+ if (globalCodexContent.includes("[features]")) {
305
+ globalCodexContent = globalCodexContent.replace(/(\[features\])/, "$1\ncodex_hooks = true");
306
+ } else {
307
+ globalCodexContent = globalCodexContent.trimEnd() + "\n\n[features]\ncodex_hooks = true";
308
+ }
309
+ }
310
+
311
+ // Remove old patchcord stop hook entry (handles updates — path may change across npm cache installs)
312
+ globalCodexContent = globalCodexContent.replace(/\[\[hooks\.Stop\]\]\nhooks = \[.*patchcord-stop-hook.*\]\n?/g, "").replace(/\n{3,}/g, "\n\n").trim();
313
+
314
+ // Add fresh entry
315
+ globalCodexContent = globalCodexContent.trimEnd() + `\n\n[[hooks.Stop]]\nhooks = [{ type = "command", command = "${hookScriptDest}", timeout = 10 }]\n`;
316
+
317
+ globalChanges.push(`Codex stop hook ${hookAlreadyExisted ? "updated" : "installed"}`);
318
+ }
319
+
320
+ writeFileSync(codexConfig, globalCodexContent);
292
321
  }
293
322
 
294
323
  // Only show global changes if something actually changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.18",
3
+ "version": "0.5.19",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Codex Stop hook — checks patchcord inbox after each turn.
5
+ # Installed automatically by `npx patchcord` when Codex is detected.
6
+
7
+ command -v jq >/dev/null 2>&1 || exit 0
8
+
9
+ INPUT=$(cat)
10
+
11
+ PROJECT_CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
12
+ [ -z "$PROJECT_CWD" ] || [ "$PROJECT_CWD" = "null" ] && PROJECT_CWD="$PWD"
13
+
14
+ # Guard: stop_hook_active prevents infinite continuation loops
15
+ STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
16
+ if [ "$STOP_ACTIVE" = "true" ]; then
17
+ exit 0
18
+ fi
19
+
20
+ # ── Resolve token + base URL from .codex/config.toml ─────────────────────────
21
+ TOKEN=""
22
+ URL=""
23
+ CODEX_TOML="$PROJECT_CWD/.codex/config.toml"
24
+
25
+ if [ -f "$CODEX_TOML" ]; then
26
+ read -r URL TOKEN < <(python3 - "$CODEX_TOML" 2>/dev/null <<'PYEOF' || true
27
+ import re, sys
28
+ content = open(sys.argv[1]).read()
29
+ url_m = re.search(r'\[mcp_servers\.patchcord[^\]]*\].*?url\s*=\s*"([^"]+)"', content, re.DOTALL)
30
+ auth_m = re.search(r'"Authorization"\s*=\s*"Bearer\s+([^"]+)"', content)
31
+ if url_m and auth_m:
32
+ base = re.sub(r'/(mcp|sse).*$', '', url_m.group(1))
33
+ print(base, auth_m.group(1).strip())
34
+ PYEOF
35
+ ) || true
36
+ fi
37
+
38
+ if [ -z "$URL" ] || [ -z "$TOKEN" ]; then
39
+ exit 0
40
+ fi
41
+
42
+ # ── Check pending count ───────────────────────────────────────────────────────
43
+ HTTP_CODE=$(curl -s -o /tmp/patchcord_codex_inbox.json -w "%{http_code}" --max-time 5 \
44
+ -H "Authorization: Bearer ${TOKEN}" \
45
+ "${URL}/api/inbox?status=pending&limit=5&count_only=1" 2>/dev/null || echo "000")
46
+
47
+ if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
48
+ jq -n '{"decision":"block","reason":"Patchcord token rejected (HTTP '"$HTTP_CODE"'). Check .codex/config.toml — re-run npx patchcord to fix."}'
49
+ rm -f /tmp/patchcord_codex_inbox.json
50
+ exit 0
51
+ fi
52
+
53
+ if [ "$HTTP_CODE" = "000" ]; then
54
+ rm -f /tmp/patchcord_codex_inbox.json
55
+ exit 0
56
+ fi
57
+
58
+ RESPONSE=$(cat /tmp/patchcord_codex_inbox.json 2>/dev/null || echo '{"count":0}')
59
+ rm -f /tmp/patchcord_codex_inbox.json
60
+
61
+ COUNT=$(echo "$RESPONSE" | jq -r '.count // .pending_count // 0' 2>/dev/null || echo "0")
62
+
63
+ if [ "$COUNT" -gt 0 ]; then
64
+ NOTIFY_LOCK="/tmp/patchcord_codex_notify_lock"
65
+ if [ -f "$NOTIFY_LOCK" ]; then
66
+ LOCK_MTIME=$(stat -c %Y "$NOTIFY_LOCK" 2>/dev/null || stat -f %m "$NOTIFY_LOCK" 2>/dev/null || echo "0")
67
+ NOW=$(date +%s)
68
+ [ $(( NOW - LOCK_MTIME )) -lt 5 ] && exit 0
69
+ fi
70
+ touch "$NOTIFY_LOCK"
71
+ jq -n --arg count "$COUNT" '{"decision":"block","reason":($count + " patchcord message(s) waiting. Call inbox() and reply to all.")}'
72
+ fi