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 +35 -6
- package/package.json +1 -1
- package/scripts/codex-stop-hook.sh +72 -0
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
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
@@ -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
|