patchcord 0.3.4 → 0.3.6
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/README.md +49 -53
- package/bin/patchcord.mjs +11 -0
- package/package.json +1 -1
- package/scripts/statusline.sh +68 -131
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchcord",
|
|
3
3
|
"description": "Cross-machine agent messaging with auto-inbox checking. Agents automatically respond to messages from other agents without human intervention.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ppravdin"
|
|
7
7
|
},
|
package/README.md
CHANGED
|
@@ -2,32 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
Cross-machine messaging between Claude Code agents.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This plugin is not the connection itself.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
npx patchcord@latest install
|
|
9
|
-
```
|
|
7
|
+
The plugin provides:
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
- Patchcord skills
|
|
10
|
+
- statusline integration
|
|
11
|
+
- turn-end inbox checks
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
The actual Patchcord connection must still come from the current project configuration.
|
|
14
|
+
|
|
15
|
+
## Safe model
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Use this plugin with project-local Patchcord config.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Good:
|
|
20
20
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
21
|
+
- install the plugin once
|
|
22
|
+
- keep `.mcp.json` inside each Patchcord-enabled project
|
|
23
|
+
- let the plugin no-op in projects that do not have Patchcord configured
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Bad:
|
|
26
|
+
|
|
27
|
+
- exporting `PATCHCORD_TOKEN` / `PATCHCORD_URL` globally in `~/.bashrc`, `~/.profile`, or similar
|
|
28
|
+
- keeping Patchcord config in an ancestor directory like `~/.mcp.json`
|
|
29
|
+
- assuming the plugin should make every project a Patchcord project
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
### 1. Install the plugin
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
claude plugin marketplace add /path/to/patchcord-internal
|
|
37
|
+
claude plugin install patchcord@patchcord-marketplace
|
|
38
|
+
```
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
### 2. Configure the project
|
|
28
41
|
|
|
29
42
|
Create a project-local `.mcp.json` in the project that should act as a Patchcord agent.
|
|
30
43
|
|
|
44
|
+
Example:
|
|
45
|
+
|
|
31
46
|
```json
|
|
32
47
|
{
|
|
33
48
|
"mcpServers": {
|
|
@@ -35,17 +50,16 @@ Create a project-local `.mcp.json` in the project that should act as a Patchcord
|
|
|
35
50
|
"type": "http",
|
|
36
51
|
"url": "https://patchcord.yourdomain.com/mcp",
|
|
37
52
|
"headers": {
|
|
38
|
-
"Authorization": "Bearer <project-token>"
|
|
39
|
-
"X-Patchcord-Client-Type": "claude_code"
|
|
53
|
+
"Authorization": "Bearer <project-token>"
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
57
|
}
|
|
44
58
|
```
|
|
45
59
|
|
|
46
|
-
### 3.
|
|
60
|
+
### 3. Restart Claude Code in that project
|
|
47
61
|
|
|
48
|
-
The plugin and statusline scripts read the current project configuration
|
|
62
|
+
The plugin and statusline scripts read the current project configuration when the session starts.
|
|
49
63
|
|
|
50
64
|
## What happens in non-Patchcord projects
|
|
51
65
|
|
|
@@ -55,53 +69,35 @@ Nothing Patchcord-specific should appear.
|
|
|
55
69
|
- no inbox checks
|
|
56
70
|
- no hook-driven Patchcord prompts
|
|
57
71
|
|
|
58
|
-
The plugin
|
|
72
|
+
The plugin is allowed to stay installed globally, but it must no-op unless the current project is configured.
|
|
59
73
|
|
|
60
74
|
## Self-hosted server
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Bearer-token clients can also use `/mcp/bearer` if you want the dedicated bearer-only endpoint.
|
|
65
|
-
|
|
66
|
-
## What the plugin provides
|
|
67
|
-
|
|
68
|
-
- Stop hook / turn-end inbox check
|
|
69
|
-
- Patchcord skill for Claude
|
|
70
|
-
- statusline identity display
|
|
76
|
+
The project `.mcp.json` should point to your own server URL:
|
|
71
77
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Without `--full`:
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
ds@default (thick) 2 msg
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
With `--full`:
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
Opus 4.6 │ 73% │ myproject (main) │ ds@default (thick) 2 msg
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"patchcord": {
|
|
82
|
+
"type": "http",
|
|
83
|
+
"url": "https://patchcord.yourdomain.com/mcp",
|
|
84
|
+
"headers": {
|
|
85
|
+
"Authorization": "Bearer <project-token>"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
94
90
|
```
|
|
95
91
|
|
|
96
92
|
## Verify
|
|
97
93
|
|
|
98
94
|
In a Patchcord-enabled project:
|
|
99
95
|
|
|
100
|
-
- statusline should show the Patchcord identity
|
|
96
|
+
- statusline should show the Patchcord identity
|
|
101
97
|
- `inbox()` should return the expected `namespace_id` and `agent_id`
|
|
102
98
|
|
|
103
99
|
In an unrelated project:
|
|
104
100
|
|
|
105
|
-
- statusline should
|
|
101
|
+
- statusline should not show Patchcord identity
|
|
106
102
|
- no Patchcord hooks should fire
|
|
107
103
|
- no Patchcord tools should be present unless that project is configured
|
package/bin/patchcord.mjs
CHANGED
|
@@ -77,6 +77,17 @@ Then run: patchcord install`);
|
|
|
77
77
|
process.exit(1);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// Disable patchcord as Codex ChatGPT app (prevents OAuth identity conflict)
|
|
81
|
+
const codexConfig = join(process.env.HOME || "", ".codex", "config.toml");
|
|
82
|
+
if (existsSync(codexConfig)) {
|
|
83
|
+
const { readFileSync, writeFileSync } = await import("fs");
|
|
84
|
+
const content = readFileSync(codexConfig, "utf-8");
|
|
85
|
+
if (!content.includes("[apps.patchcord]")) {
|
|
86
|
+
writeFileSync(codexConfig, content.trimEnd() + "\n\n[apps.patchcord]\nenabled = false\n");
|
|
87
|
+
console.log("✓ Disabled patchcord ChatGPT app in Codex (prevents identity conflict).");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
// Enable statusline
|
|
81
92
|
const enableScript = join(pluginRoot, "scripts", "enable-statusline.sh");
|
|
82
93
|
if (existsSync(enableScript)) {
|
package/package.json
CHANGED
package/scripts/statusline.sh
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Patchcord statusline for Claude Code.
|
|
3
|
-
#
|
|
4
|
-
# Default: shows only patchcord identity + inbox count.
|
|
5
|
-
# With --full: also shows model, context%, repo (branch).
|
|
6
|
-
#
|
|
7
|
-
# --full mode model/context/git display based on claude-statusline by Kamran Ahmed
|
|
8
|
-
# https://github.com/kamranahmedse/claude-statusline (MIT)
|
|
9
|
-
#
|
|
3
|
+
# Shows: model | context% | repo (branch) | agent@host [N msg]
|
|
10
4
|
# Receives session JSON on stdin, outputs ANSI-formatted text.
|
|
11
5
|
set -f
|
|
12
6
|
|
|
13
|
-
FULL=false
|
|
14
|
-
for arg in "$@"; do
|
|
15
|
-
[ "$arg" = "--full" ] && FULL=true
|
|
16
|
-
done
|
|
17
|
-
|
|
18
7
|
find_patchcord_mcp_json() {
|
|
19
8
|
local dir="$1"
|
|
20
9
|
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
|
|
@@ -30,6 +19,7 @@ find_patchcord_mcp_json() {
|
|
|
30
19
|
input=$(cat)
|
|
31
20
|
|
|
32
21
|
if [ -z "$input" ]; then
|
|
22
|
+
printf "Claude"
|
|
33
23
|
exit 0
|
|
34
24
|
fi
|
|
35
25
|
|
|
@@ -46,10 +36,44 @@ reset='\033[0m'
|
|
|
46
36
|
|
|
47
37
|
sep=" ${dim}│${reset} "
|
|
48
38
|
|
|
49
|
-
# ──
|
|
39
|
+
# ── Model + context ─────────────────────────────────────
|
|
40
|
+
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
41
|
+
|
|
42
|
+
size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
|
|
43
|
+
[ "$size" -eq 0 ] 2>/dev/null && size=200000
|
|
44
|
+
|
|
45
|
+
input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
|
|
46
|
+
cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
|
|
47
|
+
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
|
|
48
|
+
current=$(( input_tokens + cache_create + cache_read ))
|
|
49
|
+
|
|
50
|
+
if [ "$size" -gt 0 ]; then
|
|
51
|
+
pct_used=$(( current * 100 / size ))
|
|
52
|
+
else
|
|
53
|
+
pct_used=0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [ "$pct_used" -ge 90 ]; then pct_color="$red"
|
|
57
|
+
elif [ "$pct_used" -ge 70 ]; then pct_color="$yellow"
|
|
58
|
+
elif [ "$pct_used" -ge 50 ]; then pct_color="$orange"
|
|
59
|
+
else pct_color="$green"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# ── Directory + git ─────────────────────────────────────
|
|
50
63
|
cwd=$(echo "$input" | jq -r '.cwd // ""')
|
|
51
64
|
[ -z "$cwd" ] || [ "$cwd" = "null" ] && cwd=$(pwd)
|
|
65
|
+
dirname=$(basename "$cwd")
|
|
66
|
+
|
|
67
|
+
git_branch=""
|
|
68
|
+
git_dirty=""
|
|
69
|
+
if git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
70
|
+
git_branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null)
|
|
71
|
+
if [ -n "$(git -C "$cwd" status --porcelain 2>/dev/null)" ]; then
|
|
72
|
+
git_dirty="*"
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
52
75
|
|
|
76
|
+
# ── Patchcord: agent identity + inbox ───────────────────
|
|
53
77
|
pc_token=""
|
|
54
78
|
pc_url=""
|
|
55
79
|
mcp_json=$(find_patchcord_mcp_json "$cwd" || true)
|
|
@@ -83,139 +107,52 @@ if [ -n "$pc_url" ] && [ -n "$pc_token" ]; then
|
|
|
83
107
|
fi
|
|
84
108
|
|
|
85
109
|
if $needs_refresh; then
|
|
86
|
-
|
|
110
|
+
response=$(curl -sf --max-time 3 \
|
|
87
111
|
-H "Authorization: Bearer $pc_token" \
|
|
88
|
-
"${pc_url}/api/inbox?status=pending&limit=50" 2>/dev/null ||
|
|
89
|
-
if [
|
|
90
|
-
pc_data=
|
|
91
|
-
echo "$
|
|
92
|
-
elif [ "$http_code" = "200" ]; then
|
|
93
|
-
pc_data=$(cat /tmp/claude/patchcord-sl-resp.json 2>/dev/null)
|
|
94
|
-
[ -n "$pc_data" ] && echo "$pc_data" > "$cache_file"
|
|
112
|
+
"${pc_url}/api/inbox?status=pending&limit=50" 2>/dev/null || true)
|
|
113
|
+
if [ -n "$response" ]; then
|
|
114
|
+
pc_data="$response"
|
|
115
|
+
echo "$response" > "$cache_file"
|
|
95
116
|
fi
|
|
96
|
-
rm -f /tmp/claude/patchcord-sl-resp.json
|
|
97
117
|
fi
|
|
98
118
|
|
|
99
119
|
if [ -n "$pc_data" ]; then
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
namespace_id=$(echo "$pc_data" | jq -r '.namespace_id // empty' 2>/dev/null)
|
|
106
|
-
machine=$(echo "$pc_data" | jq -r '.machine_name // empty' 2>/dev/null)
|
|
107
|
-
if [ -z "$machine" ] || [ "$machine" = "null" ]; then
|
|
108
|
-
machine=$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "")
|
|
109
|
-
fi
|
|
110
|
-
count=$(echo "$pc_data" | jq -r '.count // .pending_count // 0' 2>/dev/null)
|
|
111
|
-
|
|
112
|
-
if [ -n "$agent_id" ]; then
|
|
113
|
-
pc_part="${white}${agent_id}${reset}"
|
|
114
|
-
if [ -n "$namespace_id" ] && [ "$namespace_id" != "null" ]; then
|
|
115
|
-
pc_part+="${dim}@${namespace_id}${reset}"
|
|
116
|
-
fi
|
|
117
|
-
if [ -n "$machine" ]; then
|
|
118
|
-
pc_part+=" ${dim}(${machine})${reset}"
|
|
119
|
-
fi
|
|
120
|
-
fi
|
|
121
|
-
|
|
122
|
-
if [ "$count" -gt 0 ] 2>/dev/null; then
|
|
123
|
-
pc_part+=" ${red}${count} msg${reset}"
|
|
124
|
-
fi
|
|
120
|
+
agent_id=$(echo "$pc_data" | jq -r '.agent_id // empty' 2>/dev/null)
|
|
121
|
+
namespace_id=$(echo "$pc_data" | jq -r '.namespace_id // empty' 2>/dev/null)
|
|
122
|
+
machine=$(echo "$pc_data" | jq -r '.machine_name // empty' 2>/dev/null)
|
|
123
|
+
if [ -z "$machine" ] || [ "$machine" = "null" ]; then
|
|
124
|
+
machine=$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "")
|
|
125
125
|
fi
|
|
126
|
-
|
|
127
|
-
fi
|
|
126
|
+
count=$(echo "$pc_data" | jq -r '.count // .pending_count // 0' 2>/dev/null)
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
mkdir -p /tmp/claude
|
|
137
|
-
update_stale=true
|
|
138
|
-
if [ -f "$update_cache" ]; then
|
|
139
|
-
uc_mtime=$(stat -c %Y "$update_cache" 2>/dev/null || stat -f %m "$update_cache" 2>/dev/null)
|
|
140
|
-
uc_now=$(date +%s)
|
|
141
|
-
[ $(( uc_now - uc_mtime )) -lt 86400 ] && update_stale=false
|
|
142
|
-
fi
|
|
143
|
-
if $update_stale; then
|
|
144
|
-
latest=$(npm view patchcord version --json 2>/dev/null | tr -d '"' || true)
|
|
145
|
-
if [ -n "$latest" ]; then
|
|
146
|
-
echo "{\"latest\":\"$latest\"}" > "$update_cache"
|
|
128
|
+
if [ -n "$agent_id" ]; then
|
|
129
|
+
pc_part="${white}${agent_id}${reset}"
|
|
130
|
+
if [ -n "$namespace_id" ] && [ "$namespace_id" != "null" ]; then
|
|
131
|
+
pc_part+="${dim}@${namespace_id}${reset}"
|
|
132
|
+
fi
|
|
133
|
+
if [ -n "$machine" ]; then
|
|
134
|
+
pc_part+=" ${dim}(${machine})${reset}"
|
|
147
135
|
fi
|
|
148
|
-
else
|
|
149
|
-
latest=$(jq -r '.latest // ""' "$update_cache" 2>/dev/null)
|
|
150
136
|
fi
|
|
151
|
-
|
|
152
|
-
|
|
137
|
+
|
|
138
|
+
if [ "$count" -gt 0 ] 2>/dev/null; then
|
|
139
|
+
pc_part+=" ${red}${count} msg${reset}"
|
|
153
140
|
fi
|
|
154
141
|
fi
|
|
155
142
|
fi
|
|
156
143
|
|
|
157
|
-
# No patchcord config — output nothing in default mode
|
|
158
|
-
if [ -z "$pc_part" ] && [ -z "$update_part" ] && ! $FULL; then
|
|
159
|
-
exit 0
|
|
160
|
-
fi
|
|
161
|
-
|
|
162
144
|
# ── Build line ──────────────────────────────────────────
|
|
163
|
-
line=""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
|
|
173
|
-
current=$(( input_tokens + cache_create + cache_read ))
|
|
174
|
-
if [ "$size" -gt 0 ]; then
|
|
175
|
-
pct_used=$(( current * 100 / size ))
|
|
176
|
-
else
|
|
177
|
-
pct_used=0
|
|
178
|
-
fi
|
|
179
|
-
if [ "$pct_used" -ge 90 ]; then pct_color="$red"
|
|
180
|
-
elif [ "$pct_used" -ge 70 ]; then pct_color="$yellow"
|
|
181
|
-
elif [ "$pct_used" -ge 50 ]; then pct_color="$orange"
|
|
182
|
-
else pct_color="$green"
|
|
183
|
-
fi
|
|
184
|
-
|
|
185
|
-
dirname=$(basename "$cwd")
|
|
186
|
-
git_branch=""
|
|
187
|
-
git_dirty=""
|
|
188
|
-
if git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
189
|
-
git_branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null)
|
|
190
|
-
if [ -n "$(git -C "$cwd" status --porcelain 2>/dev/null)" ]; then
|
|
191
|
-
git_dirty="*"
|
|
192
|
-
fi
|
|
193
|
-
fi
|
|
194
|
-
|
|
195
|
-
line="${blue}${model_name}${reset}"
|
|
196
|
-
line+="${sep}"
|
|
197
|
-
line+="${pct_color}${pct_used}%${reset}"
|
|
145
|
+
line="${blue}${model_name}${reset}"
|
|
146
|
+
line+="${sep}"
|
|
147
|
+
line+="${pct_color}${pct_used}%${reset}"
|
|
148
|
+
line+="${sep}"
|
|
149
|
+
line+="${cyan}${dirname}${reset}"
|
|
150
|
+
if [ -n "$git_branch" ]; then
|
|
151
|
+
line+=" ${green}(${git_branch}${red}${git_dirty}${green})${reset}"
|
|
152
|
+
fi
|
|
153
|
+
if [ -n "$pc_part" ]; then
|
|
198
154
|
line+="${sep}"
|
|
199
|
-
line+="${
|
|
200
|
-
if [ -n "$git_branch" ]; then
|
|
201
|
-
line+=" ${green}(${git_branch}${red}${git_dirty}${green})${reset}"
|
|
202
|
-
fi
|
|
203
|
-
if [ -n "$pc_part" ]; then
|
|
204
|
-
line+="${sep}"
|
|
205
|
-
line+="${pc_part}"
|
|
206
|
-
fi
|
|
207
|
-
if [ -n "$update_part" ]; then
|
|
208
|
-
line+="${sep}"
|
|
209
|
-
line+="${update_part}"
|
|
210
|
-
fi
|
|
211
|
-
else
|
|
212
|
-
line="${pc_part}"
|
|
213
|
-
if [ -n "$update_part" ]; then
|
|
214
|
-
if [ -n "$line" ]; then
|
|
215
|
-
line+="${sep}"
|
|
216
|
-
fi
|
|
217
|
-
line+="${update_part}"
|
|
218
|
-
fi
|
|
155
|
+
line+="${pc_part}"
|
|
219
156
|
fi
|
|
220
157
|
|
|
221
158
|
# ── Output ──────────────────────────────────────────────
|