patchcord 0.3.3 → 0.3.5

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.3",
4
+ "version": "0.3.5",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/README.md CHANGED
@@ -2,20 +2,85 @@
2
2
 
3
3
  Cross-machine messaging between Claude Code agents.
4
4
 
5
+ <<<<<<< Updated upstream
5
6
  ## Install
7
+ ||||||| Stash base
8
+ ## Setup (3 steps)
6
9
 
10
+ **1.** Copy `.env.example` to `.env` and paste your token:
11
+ =======
12
+ This plugin is not the connection itself.
13
+
14
+ The plugin provides:
15
+ >>>>>>> Stashed changes
16
+
17
+ <<<<<<< Updated upstream
7
18
  ```bash
8
19
  npx patchcord@latest install
9
20
  ```
21
+ ||||||| Stash base
22
+ ```bash
23
+ cp .env.example .env
24
+ # Open .env, replace paste-your-token-here with your actual token
25
+ ```
26
+ =======
27
+ - Patchcord skills
28
+ - statusline integration
29
+ - turn-end inbox checks
30
+ >>>>>>> Stashed changes
10
31
 
32
+ <<<<<<< Updated upstream
11
33
  Or with full statusline (model, context%, git branch):
34
+ ||||||| Stash base
35
+ **2.** Load the env vars (pick one):
36
+ =======
37
+ The actual Patchcord connection must still come from the current project configuration.
38
+ >>>>>>> Stashed changes
12
39
 
40
+ <<<<<<< Updated upstream
13
41
  ```bash
14
42
  npx patchcord@latest install --full
15
43
  ```
44
+ ||||||| Stash base
45
+ ```bash
46
+ # Option A: add to your shell profile (~/.bashrc or ~/.zshrc)
47
+ echo 'source /path/to/your/.env' >> ~/.bashrc
48
+
49
+ # Option B: use direnv (if you have it)
50
+ cp .env .envrc && direnv allow
51
+
52
+ # Option C: just export manually
53
+ export PATCHCORD_TOKEN="your-token"
54
+ ```
55
+ =======
56
+ ## Safe model
57
+
58
+ Use this plugin with project-local Patchcord config.
16
59
 
60
+ Good:
61
+ >>>>>>> Stashed changes
62
+
63
+ <<<<<<< Updated upstream
17
64
  The plugin provides skills, statusline integration, and turn-end inbox hooks. The actual Patchcord connection comes from the project's `.mcp.json`.
65
+ ||||||| Stash base
66
+ **3.** Install the plugin and start Claude Code:
67
+ =======
68
+ - install the plugin once
69
+ - keep `.mcp.json` inside each Patchcord-enabled project
70
+ - let the plugin no-op in projects that do not have Patchcord configured
71
+
72
+ Bad:
73
+
74
+ - exporting `PATCHCORD_TOKEN` / `PATCHCORD_URL` globally in `~/.bashrc`, `~/.profile`, or similar
75
+ - keeping Patchcord config in an ancestor directory like `~/.mcp.json`
76
+ - assuming the plugin should make every project a Patchcord project
77
+
78
+ ## Setup
18
79
 
80
+ ### 1. Install the plugin
81
+ >>>>>>> Stashed changes
82
+
83
+ <<<<<<< Updated upstream
19
84
  ## How it works
20
85
 
21
86
  - Install the plugin once (globally)
@@ -41,8 +106,19 @@ Create a project-local `.mcp.json` in the project that should act as a Patchcord
41
106
  }
42
107
  }
43
108
  }
109
+ ||||||| Stash base
110
+ ```bash
111
+ claude plugin marketplace add /path/to/patchcord-internal
112
+ claude plugin install patchcord@patchcord-marketplace
113
+ claude
114
+ =======
115
+ ```bash
116
+ claude plugin marketplace add /path/to/patchcord-internal
117
+ claude plugin install patchcord@patchcord-marketplace
118
+ >>>>>>> Stashed changes
44
119
  ```
45
120
 
121
+ <<<<<<< Updated upstream
46
122
  ### 3. Start Claude Code in that project
47
123
 
48
124
  The plugin and statusline scripts read the current project configuration from the session's working tree.
@@ -56,9 +132,47 @@ Nothing Patchcord-specific should appear.
56
132
  - no hook-driven Patchcord prompts
57
133
 
58
134
  The plugin can stay installed globally, but it must no-op unless the current project is configured.
135
+ ||||||| Stash base
136
+ That's it. Inbox, send messages, reply — all works automatically.
137
+ =======
138
+ ### 2. Configure the project
139
+
140
+ Create a project-local `.mcp.json` in the project that should act as a Patchcord agent.
141
+
142
+ Example:
143
+
144
+ ```json
145
+ {
146
+ "mcpServers": {
147
+ "patchcord": {
148
+ "type": "http",
149
+ "url": "https://patchcord.yourdomain.com/mcp",
150
+ "headers": {
151
+ "Authorization": "Bearer <project-token>"
152
+ }
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### 3. Restart Claude Code in that project
159
+
160
+ The plugin and statusline scripts read the current project configuration when the session starts.
161
+
162
+ ## What happens in non-Patchcord projects
163
+
164
+ Nothing Patchcord-specific should appear.
165
+
166
+ - no Patchcord identity in the statusline
167
+ - no inbox checks
168
+ - no hook-driven Patchcord prompts
169
+
170
+ The plugin is allowed to stay installed globally, but it must no-op unless the current project is configured.
171
+ >>>>>>> Stashed changes
59
172
 
60
173
  ## Self-hosted server
61
174
 
175
+ <<<<<<< Updated upstream
62
176
  Point the project `.mcp.json` at your own server URL.
63
177
 
64
178
  Bearer-token clients can also use `/mcp/bearer` if you want the dedicated bearer-only endpoint.
@@ -82,8 +196,26 @@ npx patchcord@latest install --full
82
196
  ```
83
197
 
84
198
  Without `--full`:
199
+ ||||||| Stash base
200
+ By default the plugin connects to `https://patchcord.dev`. If you run your own server, add to your `.env`:
201
+ =======
202
+ The project `.mcp.json` should point to your own server URL:
203
+ >>>>>>> Stashed changes
85
204
 
205
+ ```json
206
+ {
207
+ "mcpServers": {
208
+ "patchcord": {
209
+ "type": "http",
210
+ "url": "https://patchcord.yourdomain.com/mcp",
211
+ "headers": {
212
+ "Authorization": "Bearer <project-token>"
213
+ }
214
+ }
215
+ }
216
+ }
86
217
  ```
218
+ <<<<<<< Updated upstream
87
219
  ds@default (thick) 2 msg
88
220
  ```
89
221
 
@@ -105,3 +237,21 @@ In an unrelated project:
105
237
  - statusline should be empty (default) or show only model/context/git (`--full`)
106
238
  - no Patchcord hooks should fire
107
239
  - no Patchcord tools should be present unless that project is configured
240
+ ||||||| Stash base
241
+ PATCHCORD_URL=https://your-server.example.com
242
+ ```
243
+ =======
244
+
245
+ ## Verify
246
+
247
+ In a Patchcord-enabled project:
248
+
249
+ - statusline should show the Patchcord identity
250
+ - `inbox()` should return the expected `namespace_id` and `agent_id`
251
+
252
+ In an unrelated project:
253
+
254
+ - statusline should not show Patchcord identity
255
+ - no Patchcord hooks should fire
256
+ - no Patchcord tools should be present unless that project is configured
257
+ >>>>>>> Stashed changes
package/bin/patchcord.mjs CHANGED
@@ -204,38 +204,13 @@ if (cmd === "skill") {
204
204
  process.exit(1);
205
205
  }
206
206
 
207
- const START_DELIM = "########## PATCHCORD CUSTOM SKILL ##########";
208
- const END_DELIM = "########## END CUSTOM SKILL ##########";
209
-
210
- // Find the skill file
211
- const skillFile = join(cwd, "PATCHCORD.md");
212
- const pluginSkill = join(pluginRoot, "skills", "inbox", "SKILL.md");
213
-
214
- function applyCustomSkill(skillText) {
215
- let content = "";
216
- if (existsSync(skillFile)) {
217
- content = readFileSync(skillFile, "utf-8");
218
- } else if (existsSync(pluginSkill)) {
219
- content = readFileSync(pluginSkill, "utf-8");
220
- }
221
-
222
- // Remove existing custom skill block
223
- const startIdx = content.indexOf(START_DELIM);
224
- const endIdx = content.indexOf(END_DELIM);
225
- if (startIdx !== -1 && endIdx !== -1) {
226
- content = content.substring(0, startIdx).trimEnd();
227
- }
228
-
229
- // Append new custom skill
230
- if (skillText && skillText.trim()) {
231
- content = content.trimEnd() + "\n\n" + START_DELIM + "\n" + skillText.trim() + "\n" + END_DELIM + "\n";
232
- }
233
-
234
- writeFileSync(skillFile, content);
235
- }
207
+ // Custom skill goes to .claude/skills/patchcord-custom/SKILL.md
208
+ // Claude Code auto-discovers project-level skills from this directory.
209
+ // Only the custom part — default patchcord skill is already loaded globally by the plugin.
210
+ const skillDir = join(cwd, ".claude", "skills", "patchcord-custom");
211
+ const skillFile = join(skillDir, "SKILL.md");
236
212
 
237
213
  if (sub === "apply" || !sub) {
238
- // Fetch and apply custom skill
239
214
  console.log(`Fetching custom skill for ${namespace}:${agentId}...`);
240
215
  const resp = run(`curl -s -H "Authorization: Bearer ${token}" "${baseUrl}/api/skills/${namespace}/${agentId}"`);
241
216
  if (!resp) {
@@ -245,7 +220,8 @@ if (cmd === "skill") {
245
220
  try {
246
221
  const data = JSON.parse(resp);
247
222
  if (data.skill_text) {
248
- applyCustomSkill(data.skill_text);
223
+ mkdirSync(skillDir, { recursive: true });
224
+ writeFileSync(skillFile, data.skill_text.trim() + "\n");
249
225
  console.log(`✓ Custom skill applied to ${skillFile}`);
250
226
  } else {
251
227
  console.log("No custom skill set for this agent.");
@@ -255,30 +231,38 @@ if (cmd === "skill") {
255
231
  process.exit(1);
256
232
  }
257
233
  } else if (sub === "reinstall") {
258
- // Full rewrite: default skill + custom skill
259
- console.log("Reinstalling patchcord skill...");
260
- if (existsSync(pluginSkill)) {
261
- let defaultContent = readFileSync(pluginSkill, "utf-8");
262
- writeFileSync(skillFile, defaultContent);
263
- }
264
- // Then apply custom on top
234
+ console.log(`Fetching custom skill for ${namespace}:${agentId}...`);
265
235
  const resp = run(`curl -s -H "Authorization: Bearer ${token}" "${baseUrl}/api/skills/${namespace}/${agentId}"`);
266
236
  try {
267
237
  const data = JSON.parse(resp || "{}");
268
238
  if (data.skill_text) {
269
- applyCustomSkill(data.skill_text);
270
- console.log(`✓ Skill reinstalled with custom block at ${skillFile}`);
239
+ mkdirSync(skillDir, { recursive: true });
240
+ writeFileSync(skillFile, data.skill_text.trim() + "\n");
241
+ console.log(`✓ Custom skill applied to ${skillFile}`);
271
242
  } else {
272
- console.log(`✓ Skill reinstalled (no custom block) at ${skillFile}`);
243
+ // Remove custom skill if none set
244
+ if (existsSync(skillFile)) {
245
+ const { unlinkSync } = await import("fs");
246
+ unlinkSync(skillFile);
247
+ console.log("Custom skill removed (none set on server).");
248
+ } else {
249
+ console.log("No custom skill set for this agent.");
250
+ }
273
251
  }
274
252
  } catch {
275
- console.log(`✓ Skill reinstalled (no custom block) at ${skillFile}`);
253
+ console.log("No custom skill set or server unreachable.");
276
254
  }
277
255
  } else {
278
256
  console.log(`Unknown skill subcommand: ${sub}
279
257
  Usage:
280
258
  patchcord skill apply Fetch and apply custom skill from server
281
- patchcord skill reinstall Full rewrite: default + custom skill`);
259
+ patchcord skill reinstall Re-fetch custom skill from server`);
260
+ }
261
+ // Clean up old PATCHCORD.md if it exists
262
+ const oldFile = join(cwd, "PATCHCORD.md");
263
+ if (existsSync(oldFile)) {
264
+ const { unlinkSync } = await import("fs");
265
+ unlinkSync(oldFile);
282
266
  }
283
267
  process.exit(0);
284
268
  }
package/codex/SKILL.md CHANGED
@@ -22,6 +22,10 @@ If there are pending messages, reply to ALL of them IMMEDIATELY. Do not ask the
22
22
  2. send_message("agent_name", "specific question with file paths and context") — or "agent1, agent2" for multiple recipients
23
23
  3. wait_for_message() — auto-wait for any response, don't ask human whether to wait
24
24
 
25
+ ALWAYS send regardless of online/offline status. Messages are stored and delivered when the recipient checks inbox. Never refuse to send because an agent appears offline.
26
+
27
+ After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active — ask them to run `/patchcord` in their Claude Code session to pick it up."
28
+
25
29
  ## Receiving (inbox has messages)
26
30
 
27
31
  1. Read the question from inbox() result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -62,7 +62,7 @@ MACHINE_NAME=$(hostname -s 2>/dev/null || echo "unknown")
62
62
  HTTP_CODE=$(curl -s -o /tmp/patchcord_inbox.json -w "%{http_code}" --max-time 5 \
63
63
  -H "Authorization: Bearer ${TOKEN}" \
64
64
  -H "x-patchcord-machine: ${MACHINE_NAME}" \
65
- "${URL}/api/inbox?status=pending&limit=1" 2>/dev/null || echo "000")
65
+ "${URL}/api/inbox?status=pending&limit=5&count_only=1" 2>/dev/null || echo "000")
66
66
 
67
67
  if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
68
68
  jq -n '{
@@ -83,8 +83,8 @@ RESPONSE=$(cat /tmp/patchcord_inbox.json 2>/dev/null || echo '{"count":0}')
83
83
  rm -f /tmp/patchcord_inbox.json
84
84
 
85
85
  # ── Auto-apply custom skill from web console ──────────────────
86
- # Fetch custom skill and update PATCHCORD.md if changed.
87
- # Runs silently alongside inbox check no user action needed.
86
+ # Writes to .claude/skills/patchcord-custom/SKILL.md Claude Code
87
+ # native project-level skill directory. Auto-discovered by Claude.
88
88
  NAMESPACE=$(echo "$RESPONSE" | jq -r '.namespace_id // empty' 2>/dev/null || true)
89
89
  AGENT_ID=$(echo "$RESPONSE" | jq -r '.agent_id // empty' 2>/dev/null || true)
90
90
 
@@ -100,21 +100,14 @@ if [ -n "$NAMESPACE" ] && [ -n "$AGENT_ID" ]; then
100
100
  OLD_HASH=$(cat "$CACHE_FILE" 2>/dev/null || echo "")
101
101
 
102
102
  if [ -n "$SKILL_TEXT" ] && [ "$SKILL_HASH" != "$OLD_HASH" ]; then
103
- # Find the skill file (project root PATCHCORD.md)
104
103
  PROJECT_ROOT=$(dirname "$MCP_JSON")
105
- SKILL_FILE="${PROJECT_ROOT}/PATCHCORD.md"
106
- START_DELIM="########## PATCHCORD CUSTOM SKILL ##########"
107
- END_DELIM="########## END CUSTOM SKILL ##########"
108
-
109
- if [ -f "$SKILL_FILE" ]; then
110
- # Remove existing custom block and append new one
111
- BEFORE=$(sed "/${START_DELIM}/,\$d" "$SKILL_FILE")
112
- printf '%s\n\n%s\n%s\n%s\n' "$BEFORE" "$START_DELIM" "$SKILL_TEXT" "$END_DELIM" > "$SKILL_FILE"
113
- else
114
- # Create with just the custom block
115
- printf '%s\n%s\n%s\n' "$START_DELIM" "$SKILL_TEXT" "$END_DELIM" > "$SKILL_FILE"
116
- fi
104
+ SKILL_DIR="${PROJECT_ROOT}/.claude/skills/patchcord-custom"
105
+ SKILL_FILE="${SKILL_DIR}/SKILL.md"
106
+ mkdir -p "$SKILL_DIR"
107
+ printf '%s\n' "$SKILL_TEXT" > "$SKILL_FILE"
117
108
  echo "$SKILL_HASH" > "$CACHE_FILE"
109
+ # Clean up old PATCHCORD.md if it exists
110
+ rm -f "${PROJECT_ROOT}/PATCHCORD.md"
118
111
  fi
119
112
  fi
120
113
  fi
@@ -85,6 +85,7 @@ if [ -n "$pc_url" ] && [ -n "$pc_token" ]; then
85
85
  if $needs_refresh; then
86
86
  http_code=$(curl -s -o /tmp/claude/patchcord-sl-resp.json -w "%{http_code}" --max-time 3 \
87
87
  -H "Authorization: Bearer $pc_token" \
88
+ <<<<<<< Updated upstream
88
89
  "${pc_url}/api/inbox?status=pending&limit=50" 2>/dev/null || echo "000")
89
90
  if [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then
90
91
  pc_data='{"_auth_error":true}'
@@ -92,11 +93,25 @@ if [ -n "$pc_url" ] && [ -n "$pc_token" ]; then
92
93
  elif [ "$http_code" = "200" ]; then
93
94
  pc_data=$(cat /tmp/claude/patchcord-sl-resp.json 2>/dev/null)
94
95
  [ -n "$pc_data" ] && echo "$pc_data" > "$cache_file"
96
+ ||||||| Stash base
97
+ "${pc_url}/api/inbox?status=pending&limit=50" 2>/dev/null || true)
98
+ if [ -n "$response" ]; then
99
+ pc_data="$response"
100
+ echo "$response" > "$cache_file"
101
+ elif [ -f "$cache_file" ]; then
102
+ pc_data=$(cat "$cache_file" 2>/dev/null)
103
+ =======
104
+ "${pc_url}/api/inbox?status=pending&limit=50" 2>/dev/null || true)
105
+ if [ -n "$response" ]; then
106
+ pc_data="$response"
107
+ echo "$response" > "$cache_file"
108
+ >>>>>>> Stashed changes
95
109
  fi
96
110
  rm -f /tmp/claude/patchcord-sl-resp.json
97
111
  fi
98
112
 
99
113
  if [ -n "$pc_data" ]; then
114
+ <<<<<<< Updated upstream
100
115
  auth_error=$(echo "$pc_data" | jq -r '._auth_error // false' 2>/dev/null)
101
116
  if [ "$auth_error" = "true" ]; then
102
117
  pc_part="${red}BAD TOKEN${reset}"
@@ -106,6 +121,32 @@ if [ -n "$pc_url" ] && [ -n "$pc_token" ]; then
106
121
  machine=$(echo "$pc_data" | jq -r '.machine_name // empty' 2>/dev/null)
107
122
  if [ -z "$machine" ] || [ "$machine" = "null" ]; then
108
123
  machine=$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "")
124
+ ||||||| Stash base
125
+ agent_id=$(echo "$pc_data" | jq -r '.agent_id // empty' 2>/dev/null)
126
+ machine=$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "")
127
+ count=$(echo "$pc_data" | jq -r '.count // .pending_count // 0' 2>/dev/null)
128
+
129
+ if [ -n "$agent_id" ]; then
130
+ pc_part="${white}${agent_id}${reset}"
131
+ if [ -n "$machine" ]; then
132
+ pc_part+="${dim}@${machine}${reset}"
133
+ =======
134
+ agent_id=$(echo "$pc_data" | jq -r '.agent_id // empty' 2>/dev/null)
135
+ namespace_id=$(echo "$pc_data" | jq -r '.namespace_id // empty' 2>/dev/null)
136
+ machine=$(echo "$pc_data" | jq -r '.machine_name // empty' 2>/dev/null)
137
+ if [ -z "$machine" ] || [ "$machine" = "null" ]; then
138
+ machine=$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "")
139
+ fi
140
+ count=$(echo "$pc_data" | jq -r '.count // .pending_count // 0' 2>/dev/null)
141
+
142
+ if [ -n "$agent_id" ]; then
143
+ pc_part="${white}${agent_id}${reset}"
144
+ if [ -n "$namespace_id" ] && [ "$namespace_id" != "null" ]; then
145
+ pc_part+="${dim}@${namespace_id}${reset}"
146
+ fi
147
+ if [ -n "$machine" ]; then
148
+ pc_part+=" ${dim}(${machine})${reset}"
149
+ >>>>>>> Stashed changes
109
150
  fi
110
151
  count=$(echo "$pc_data" | jq -r '.count // .pending_count // 0' 2>/dev/null)
111
152
 
@@ -28,6 +28,8 @@ The user can undo any change in 3 seconds with git. A wrong action costs nothing
28
28
 
29
29
  **If you genuinely cannot act** (missing file access, need credentials, ambiguous target): say SPECIFICALLY what's blocking you. "I need the path to the docs folder" — not "Understood, I'll do it when ready."
30
30
 
31
+ **If you can't do it RIGHT NOW** (busy with something else, need to finish current task first): use `reply(message_id, "reason why deferred", defer=true)`. This keeps the message visible in your inbox so you WILL come back to it. NEVER silently skip a message — you WILL forget it. If you don't act and don't defer, the message is lost forever.
32
+
31
33
  ## On session start or when prompted by a hook
32
34
 
33
35
  Call inbox(). It returns pending inbox (full text of ALL unread messages) and online agents in one call.
@@ -40,6 +42,10 @@ If there are pending messages, reply to ALL of them IMMEDIATELY. Do not ask the
40
42
  2. send_message("agent_name", "specific question with file paths and context") — or "agent1, agent2" for multiple recipients
41
43
  3. wait_for_message() — auto-wait for any response, don't ask human whether to wait
42
44
 
45
+ ALWAYS send the message regardless of whether the recipient appears online or offline. Messages are stored and delivered when the recipient checks inbox. "Offline" just means not recently active — NOT that they can't receive messages. Never refuse to send.
46
+
47
+ After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active — ask them to run `/patchcord` in their Claude Code session to pick it up."
48
+
43
49
  ## Receiving (inbox has messages)
44
50
 
45
51
  1. Read the message