patchcord 0.6.36 → 0.6.38
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/.claude-plugin/plugin.json +1 -1
- package/bin/patchcord.mjs +38 -4
- package/package.json +1 -1
- package/scripts/check-inbox.sh +35 -6
package/bin/patchcord.mjs
CHANGED
|
@@ -1735,6 +1735,7 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1735
1735
|
// supersede anything.
|
|
1736
1736
|
if (!forceRemint) {
|
|
1737
1737
|
let reusable = null;
|
|
1738
|
+
let undetermined = null; // a candidate we could NOT judge — see below
|
|
1738
1739
|
try {
|
|
1739
1740
|
const { listProjectBearers } = await import(
|
|
1740
1741
|
new URL("../scripts/lib/resolve-project-bearer.mjs", import.meta.url).href
|
|
@@ -1747,7 +1748,27 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1747
1748
|
seen.add(cand.token);
|
|
1748
1749
|
const baseUrl = String(cand.url || "").replace(/\/mcp(\/bearer)?$/, "") || DEFAULT_API;
|
|
1749
1750
|
const probe = await _httpJSON("GET", `${baseUrl}/api/agent/whoami`, cand.token);
|
|
1750
|
-
|
|
1751
|
+
const code = String(probe.status ?? "");
|
|
1752
|
+
|
|
1753
|
+
// THREE outcomes, not two. Collapsing them is the whole bug class this
|
|
1754
|
+
// command exists to stop causing:
|
|
1755
|
+
//
|
|
1756
|
+
// 401/403 the credential is genuinely dead (revoked in the web
|
|
1757
|
+
// console, or superseded). Skip it; minting is CORRECT.
|
|
1758
|
+
// 200 alive — check whose it is below.
|
|
1759
|
+
// anything else (000 from a failed connection, null when curl itself
|
|
1760
|
+
// died, any 5xx) we DO NOT KNOW. Treating "could not ask"
|
|
1761
|
+
// as "dead" would skip a HEALTHY token and mint, which
|
|
1762
|
+
// destroys the live session this command exists to protect
|
|
1763
|
+
// — and it would do that exactly when the backend is
|
|
1764
|
+
// already unwell. Absent, dead and unavailable are three
|
|
1765
|
+
// different answers.
|
|
1766
|
+
if (code === "401" || code === "403") continue;
|
|
1767
|
+
if (code !== "200") {
|
|
1768
|
+
undetermined = { token: cand.token, base: baseUrl, code: code || "no response" };
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1751
1772
|
const who = probe.json || {};
|
|
1752
1773
|
const gotNs = who.namespace_id || who.namespace || (who.self && who.self.namespace_id);
|
|
1753
1774
|
const gotAgent = who.agent_id || who.agent || (who.self && who.self.agent_id);
|
|
@@ -1760,9 +1781,22 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1760
1781
|
}
|
|
1761
1782
|
}
|
|
1762
1783
|
} catch (e) {
|
|
1763
|
-
//
|
|
1764
|
-
//
|
|
1765
|
-
|
|
1784
|
+
// A crash in the reuse scan is itself "undetermined", not "dead". Same
|
|
1785
|
+
// reasoning: never let a local failure authorise a destructive remint.
|
|
1786
|
+
undetermined = undetermined || { code: `scan failed: ${e?.message || e}` };
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// Could not determine, and found nothing provably alive: STOP. Do not
|
|
1790
|
+
// mint. Minting is the irreversible half of this command — it kills a
|
|
1791
|
+
// credential a running session may be holding — and an unreachable server
|
|
1792
|
+
// is not evidence that anything needs replacing. Retrying is free; a
|
|
1793
|
+
// revoked live session is not.
|
|
1794
|
+
if (!reusable && undetermined) {
|
|
1795
|
+
console.error(`✗ cannot verify the existing credential for ${M.green}${ns}:${arg}${M.rst} (${undetermined.code}).`);
|
|
1796
|
+
console.error(` Refusing to mint a replacement: that would revoke the token any`);
|
|
1797
|
+
console.error(` running session for this agent is currently using.`);
|
|
1798
|
+
console.error(` ${M.dim}Retry when the server is reachable, or force it deliberately with --force-remint.${M.rst}`);
|
|
1799
|
+
process.exit(1);
|
|
1766
1800
|
}
|
|
1767
1801
|
if (reusable) {
|
|
1768
1802
|
writeWorkerConfig(tool, dir, reusable.base, reusable.token, hostname);
|
package/package.json
CHANGED
package/scripts/check-inbox.sh
CHANGED
|
@@ -75,7 +75,22 @@ fi
|
|
|
75
75
|
# Check inbox — one lightweight HTTP call
|
|
76
76
|
MACHINE_NAME=$(hostname -s 2>/dev/null || echo "unknown")
|
|
77
77
|
INSTALL_PATH=$(dirname "$MCP_JSON")
|
|
78
|
-
|
|
78
|
+
|
|
79
|
+
# PER-INVOCATION response file. This was a FIXED path (/tmp/patchcord_inbox.json)
|
|
80
|
+
# shared by every agent on the machine, and a host running several seats stops
|
|
81
|
+
# them at overlapping times: one hook's curl truncates and rewrites the file
|
|
82
|
+
# another hook is reading, and either one's `rm -f` deletes it underneath the
|
|
83
|
+
# other. The reader then hands a half-written body to jq, which is exactly the
|
|
84
|
+
# "Invalid numeric literal at line 1, column N" this produced on the Mac mini.
|
|
85
|
+
# A shared mutable temp path between concurrent agents is not a race that
|
|
86
|
+
# happens rarely; it is one that happens whenever the machine is busy.
|
|
87
|
+
RESP_FILE=$(mktemp "${TMPDIR:-/tmp}/patchcord_inbox.XXXXXX" 2>/dev/null || echo "")
|
|
88
|
+
[ -z "$RESP_FILE" ] && exit 0
|
|
89
|
+
# Remove it on ANY exit path, including the early returns below, so a crash
|
|
90
|
+
# cannot leave per-invocation files accumulating in /tmp.
|
|
91
|
+
trap 'rm -f "$RESP_FILE"' EXIT
|
|
92
|
+
|
|
93
|
+
HTTP_CODE=$(curl -s -o "$RESP_FILE" -w "%{http_code}" --max-time 5 \
|
|
79
94
|
-H "Authorization: Bearer ${TOKEN}" \
|
|
80
95
|
-H "x-patchcord-machine: ${MACHINE_NAME}" \
|
|
81
96
|
-H "x-patchcord-install-path: ${INSTALL_PATH}" \
|
|
@@ -86,18 +101,25 @@ if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
|
|
|
86
101
|
"decision": "block",
|
|
87
102
|
"reason": "PATCHCORD AUTH FAILED: token rejected by server (HTTP '"$HTTP_CODE"'). Check your token in .mcp.json — it may be wrong, expired, or not yet registered on the server."
|
|
88
103
|
}'
|
|
89
|
-
rm -f /tmp/patchcord_inbox.json
|
|
90
104
|
exit 0
|
|
91
105
|
fi
|
|
92
106
|
|
|
93
107
|
if [ "$HTTP_CODE" = "000" ]; then
|
|
94
108
|
# Server unreachable — skip silently
|
|
95
|
-
rm -f /tmp/patchcord_inbox.json
|
|
96
109
|
exit 0
|
|
97
110
|
fi
|
|
98
111
|
|
|
99
|
-
RESPONSE=$(cat
|
|
100
|
-
|
|
112
|
+
RESPONSE=$(cat "$RESP_FILE" 2>/dev/null || echo "")
|
|
113
|
+
|
|
114
|
+
# "COULD NOT TELL" IS NOT "NOTHING PENDING". Below, an unparseable body used to
|
|
115
|
+
# fall through jq's `// 0` default to COUNT=0, which takes the same exit path as
|
|
116
|
+
# a genuinely empty inbox AND clears the streak state. So a corrupted response
|
|
117
|
+
# told the agent it had no messages, silently, with no way to tell the two apart
|
|
118
|
+
# — the same absence-reads-as-a-value shape that cost this project a night.
|
|
119
|
+
# Refuse to answer instead: leave the streak state alone and say nothing.
|
|
120
|
+
if ! printf '%s' "$RESPONSE" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
101
123
|
|
|
102
124
|
# ── Auto-apply custom skill from web console ──────────────────
|
|
103
125
|
# Writes to .claude/skills/patchcord-custom/SKILL.md — Claude Code
|
|
@@ -142,7 +164,14 @@ if [ "$COUNT" -eq 0 ]; then
|
|
|
142
164
|
exit 0
|
|
143
165
|
fi
|
|
144
166
|
|
|
145
|
-
|
|
167
|
+
# PER-IDENTITY lock. This was a single global path, so on a host running several
|
|
168
|
+
# seats one agent's notification suppressed EVERY other agent's for 5s — an
|
|
169
|
+
# agent with real pending messages could be silenced by an unrelated seat that
|
|
170
|
+
# happened to stop first. The dedupe is meant to stop ONE identity being told
|
|
171
|
+
# twice by the Stop and Notification hooks, never to stop a second identity
|
|
172
|
+
# being told at all.
|
|
173
|
+
NOTIFY_LOCK=$(pc_state_path "patchcord_notify_lock" "$NAMESPACE" "$AGENT_ID" 2>/dev/null || echo "")
|
|
174
|
+
[ -z "$NOTIFY_LOCK" ] && NOTIFY_LOCK="${TMPDIR:-/tmp}/patchcord_notify_lock_$(printf '%s' "${NAMESPACE}/${AGENT_ID}" | tr -c 'a-zA-Z0-9' '_')"
|
|
146
175
|
LOCK_AGE=5
|
|
147
176
|
if [ -f "$NOTIFY_LOCK" ]; then
|
|
148
177
|
LOCK_MTIME=$(stat -c %Y "$NOTIFY_LOCK" 2>/dev/null || stat -f %m "$NOTIFY_LOCK" 2>/dev/null || echo "0")
|