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.
@@ -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",
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
- ## Install
5
+ This plugin is not the connection itself.
6
6
 
7
- ```bash
8
- npx patchcord@latest install
9
- ```
7
+ The plugin provides:
10
8
 
11
- Or with full statusline (model, context%, git branch):
9
+ - Patchcord skills
10
+ - statusline integration
11
+ - turn-end inbox checks
12
12
 
13
- ```bash
14
- npx patchcord@latest install --full
15
- ```
13
+ The actual Patchcord connection must still come from the current project configuration.
14
+
15
+ ## Safe model
16
16
 
17
- The plugin provides skills, statusline integration, and turn-end inbox hooks. The actual Patchcord connection comes from the project's `.mcp.json`.
17
+ Use this plugin with project-local Patchcord config.
18
18
 
19
- ## How it works
19
+ Good:
20
20
 
21
- - Install the plugin once (globally)
22
- - Keep `.mcp.json` inside each Patchcord-enabled project
23
- - The plugin no-ops in projects without Patchcord configured
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
- Don't export `PATCHCORD_TOKEN` / `PATCHCORD_URL` globally or put config in `~/.mcp.json`.
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
- ## Configure the project
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. Start Claude Code in that project
60
+ ### 3. Restart Claude Code in that project
47
61
 
48
- The plugin and statusline scripts read the current project configuration from the session's working tree.
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 can stay installed globally, but it must no-op unless the current project is configured.
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
- Point the project `.mcp.json` at your own server URL.
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
- The MCP tools themselves come from the project's `.mcp.json` server connection, not from the plugin bundle.
73
-
74
- ## Statusline
75
-
76
- By default the statusline shows only Patchcord identity and inbox count. In non-Patchcord projects it outputs nothing.
77
-
78
- To also show model, context usage, repo, and git branch:
79
-
80
- ```bash
81
- npx patchcord@latest install --full
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 and pending message count
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 be empty (default) or show only model/context/git (`--full`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -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
- # ── Patchcord: agent identity + inbox ───────────────────
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
- http_code=$(curl -s -o /tmp/claude/patchcord-sl-resp.json -w "%{http_code}" --max-time 3 \
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 || echo "000")
89
- if [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then
90
- pc_data='{"_auth_error":true}'
91
- echo "$pc_data" > "$cache_file"
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
- auth_error=$(echo "$pc_data" | jq -r '._auth_error // false' 2>/dev/null)
101
- if [ "$auth_error" = "true" ]; then
102
- pc_part="${red}BAD TOKEN${reset}"
103
- else
104
- agent_id=$(echo "$pc_data" | jq -r '.agent_id // empty' 2>/dev/null)
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
- fi
127
- fi
126
+ count=$(echo "$pc_data" | jq -r '.count // .pending_count // 0' 2>/dev/null)
128
127
 
129
- # ── Update check (once per 24h) ───────────────────────
130
- update_part=""
131
- plugin_json="${CLAUDE_PLUGIN_ROOT:-.}/.claude-plugin/plugin.json"
132
- if [ -f "$plugin_json" ]; then
133
- installed_ver=$(jq -r '.version // ""' "$plugin_json" 2>/dev/null)
134
- if [ -n "$installed_ver" ]; then
135
- update_cache="/tmp/claude/patchcord-update-check.json"
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
- if [ -n "$latest" ] && [ "$latest" != "$installed_ver" ]; then
152
- update_part="${yellow}⬆ ${latest} (npm update -g patchcord)${reset}"
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
- if $FULL; then
166
- model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
167
-
168
- size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
169
- [ "$size" -eq 0 ] 2>/dev/null && size=200000
170
- input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
171
- cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
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+="${cyan}${dirname}${reset}"
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 ──────────────────────────────────────────────