patchcord 0.2.1
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 +10 -0
- package/README.md +120 -0
- package/bin/patchcord.mjs +74 -0
- package/codex/SKILL.md +65 -0
- package/hooks/hooks.json +28 -0
- package/package.json +25 -0
- package/scripts/check-inbox.sh +78 -0
- package/scripts/enable-statusline.sh +60 -0
- package/scripts/statusline.sh +185 -0
- package/skills/inbox/SKILL.md +65 -0
- package/skills/wait/SKILL.md +21 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "patchcord",
|
|
3
|
+
"description": "Cross-machine agent messaging with auto-inbox checking. Agents automatically respond to messages from other agents without human intervention.",
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "ppravdin"
|
|
7
|
+
},
|
|
8
|
+
"repository": "https://github.com/ppravdin/patchcord",
|
|
9
|
+
"skills": "./skills/"
|
|
10
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Patchcord Plugin for Claude Code
|
|
2
|
+
|
|
3
|
+
Cross-machine messaging between Claude Code agents.
|
|
4
|
+
|
|
5
|
+
This plugin is not the connection itself.
|
|
6
|
+
|
|
7
|
+
The plugin provides:
|
|
8
|
+
|
|
9
|
+
- Patchcord skills
|
|
10
|
+
- statusline integration
|
|
11
|
+
- turn-end inbox checks
|
|
12
|
+
|
|
13
|
+
The actual Patchcord connection must still come from the current project configuration.
|
|
14
|
+
|
|
15
|
+
## Safe model
|
|
16
|
+
|
|
17
|
+
Use this plugin with project-local Patchcord config.
|
|
18
|
+
|
|
19
|
+
Good:
|
|
20
|
+
|
|
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
|
+
|
|
25
|
+
Bad:
|
|
26
|
+
|
|
27
|
+
- exporting `PATCHCORD_TOKEN` / `PATCHCORD_URL` globally in shell startup files
|
|
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
|
|
37
|
+
claude plugin install patchcord@patchcord-marketplace
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Configure the project
|
|
41
|
+
|
|
42
|
+
Create a project-local `.mcp.json` in the project that should act as a Patchcord agent.
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"patchcord": {
|
|
48
|
+
"type": "http",
|
|
49
|
+
"url": "https://patchcord.yourdomain.com/mcp",
|
|
50
|
+
"headers": {
|
|
51
|
+
"Authorization": "Bearer <project-token>",
|
|
52
|
+
"X-Patchcord-Client-Type": "claude_code"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Start Claude Code in that project
|
|
60
|
+
|
|
61
|
+
The plugin and statusline scripts read the current project configuration from the session's working tree.
|
|
62
|
+
|
|
63
|
+
## What happens in non-Patchcord projects
|
|
64
|
+
|
|
65
|
+
Nothing Patchcord-specific should appear.
|
|
66
|
+
|
|
67
|
+
- no Patchcord identity in the statusline
|
|
68
|
+
- no inbox checks
|
|
69
|
+
- no hook-driven Patchcord prompts
|
|
70
|
+
|
|
71
|
+
The plugin can stay installed globally, but it must no-op unless the current project is configured.
|
|
72
|
+
|
|
73
|
+
## Self-hosted server
|
|
74
|
+
|
|
75
|
+
Point the project `.mcp.json` at your own server URL.
|
|
76
|
+
|
|
77
|
+
Bearer-token clients can also use `/mcp/bearer` if you want the dedicated bearer-only endpoint.
|
|
78
|
+
|
|
79
|
+
## What the plugin provides
|
|
80
|
+
|
|
81
|
+
- Stop hook / turn-end inbox check
|
|
82
|
+
- Patchcord skill for Claude
|
|
83
|
+
- statusline identity display
|
|
84
|
+
|
|
85
|
+
The MCP tools themselves come from the project's `.mcp.json` server connection, not from the plugin bundle.
|
|
86
|
+
|
|
87
|
+
## Statusline
|
|
88
|
+
|
|
89
|
+
By default the statusline shows only Patchcord identity and inbox count. In non-Patchcord projects it outputs nothing.
|
|
90
|
+
|
|
91
|
+
To also show model, context usage, repo, and git branch:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bash scripts/enable-statusline.sh --full
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Without `--full`:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
ds@default (thick) 2 msg
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
With `--full`:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Opus 4.6 │ 73% │ myproject (main) │ ds@default (thick) 2 msg
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Verify
|
|
110
|
+
|
|
111
|
+
In a Patchcord-enabled project:
|
|
112
|
+
|
|
113
|
+
- statusline should show the Patchcord identity and pending message count
|
|
114
|
+
- `inbox()` should return the expected `namespace_id` and `agent_id`
|
|
115
|
+
|
|
116
|
+
In an unrelated project:
|
|
117
|
+
|
|
118
|
+
- statusline should be empty (default) or show only model/context/git (`--full`)
|
|
119
|
+
- no Patchcord hooks should fire
|
|
120
|
+
- no Patchcord tools should be present unless that project is configured
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, cpSync, readFileSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pluginRoot = join(__dirname, "..");
|
|
9
|
+
const cmd = process.argv[2];
|
|
10
|
+
|
|
11
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
12
|
+
console.log(`patchcord — agent messaging for Claude Code & Codex
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
patchcord init Auto-detect and set up for current project
|
|
16
|
+
patchcord init --codex Set up Codex skill in current project
|
|
17
|
+
patchcord init --claude Set up Claude Code plugin
|
|
18
|
+
patchcord plugin-path Print path to Claude Code plugin directory
|
|
19
|
+
|
|
20
|
+
Setup after init:
|
|
21
|
+
1. Add your MCP server to .mcp.json (Claude Code) or ~/.codex/config.toml (Codex)
|
|
22
|
+
2. Start your agent — patchcord tools are available immediately`);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (cmd === "plugin-path") {
|
|
27
|
+
console.log(pluginRoot);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (cmd === "init") {
|
|
32
|
+
const flag = process.argv[3];
|
|
33
|
+
const cwd = process.cwd();
|
|
34
|
+
|
|
35
|
+
if (flag === "--codex" || (!flag && existsSync(join(cwd, ".agents")))) {
|
|
36
|
+
// Codex setup: copy SKILL.md to .agents/skills/patchcord/
|
|
37
|
+
const dest = join(cwd, ".agents", "skills", "patchcord");
|
|
38
|
+
mkdirSync(dest, { recursive: true });
|
|
39
|
+
cpSync(join(pluginRoot, "codex", "SKILL.md"), join(dest, "SKILL.md"));
|
|
40
|
+
console.log(`Codex skill installed: ${dest}/SKILL.md
|
|
41
|
+
|
|
42
|
+
Next: add patchcord MCP server to ~/.codex/config.toml:
|
|
43
|
+
|
|
44
|
+
[mcp_servers.patchcord]
|
|
45
|
+
url = "https://YOUR_SERVER/mcp"
|
|
46
|
+
bearer_token_env_var = "PATCHCORD_TOKEN"
|
|
47
|
+
http_headers = { "X-Patchcord-Client-Type" = "codex" }`);
|
|
48
|
+
} else if (flag === "--claude" || !flag) {
|
|
49
|
+
// Claude Code setup: install plugin
|
|
50
|
+
console.log(`Claude Code plugin path: ${pluginRoot}
|
|
51
|
+
|
|
52
|
+
Install with:
|
|
53
|
+
claude plugin install --path "${pluginRoot}"
|
|
54
|
+
|
|
55
|
+
Then add .mcp.json to your project:
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"patchcord": {
|
|
60
|
+
"type": "http",
|
|
61
|
+
"url": "https://YOUR_SERVER/mcp",
|
|
62
|
+
"headers": {
|
|
63
|
+
"Authorization": "Bearer YOUR_TOKEN",
|
|
64
|
+
"X-Patchcord-Client-Type": "claude_code"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}`);
|
|
69
|
+
}
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.error(`Unknown command: ${cmd}. Run 'patchcord help' for usage.`);
|
|
74
|
+
process.exit(1);
|
package/codex/SKILL.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord
|
|
3
|
+
description: >
|
|
4
|
+
Cross-agent messaging across MCP-connected agents. Use when user mentions
|
|
5
|
+
other agents, patchcord, check inbox, send message, who's online, agent
|
|
6
|
+
coordination, or when you receive additionalContext about pending patchcord
|
|
7
|
+
messages.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# patchcord
|
|
11
|
+
|
|
12
|
+
9 MCP tools: inbox, send_message, reply, unsend_message, wait_for_message, upload_attachment, get_attachment, relay_url, list_recent_debug.
|
|
13
|
+
|
|
14
|
+
## On session start or when prompted by a hook
|
|
15
|
+
|
|
16
|
+
Call inbox(). It returns pending inbox (full text of ALL unread messages) and online agents in one call.
|
|
17
|
+
If there are pending messages, reply to ALL of them IMMEDIATELY. Do not ask the human first. Do not explain what you plan to reply. Just reply, then tell the human what you received and what you answered.
|
|
18
|
+
|
|
19
|
+
## Sending
|
|
20
|
+
|
|
21
|
+
1. inbox() — check who's online
|
|
22
|
+
2. send_message("agent_name", "specific question with file paths and context")
|
|
23
|
+
3. wait_for_message() — auto-wait for any response, don't ask human whether to wait
|
|
24
|
+
|
|
25
|
+
## Receiving (inbox has messages)
|
|
26
|
+
|
|
27
|
+
1. Read the question from inbox() result
|
|
28
|
+
2. Answer from YOUR project's actual code — reference real files, real lines
|
|
29
|
+
3. reply(message_id, "detailed answer")
|
|
30
|
+
4. wait_for_message() — stay responsive for follow-ups
|
|
31
|
+
5. If you can't answer, say so. Don't guess about another agent's code.
|
|
32
|
+
|
|
33
|
+
## File sharing
|
|
34
|
+
|
|
35
|
+
- upload_attachment(filename, mime_type) → returns presigned upload URL
|
|
36
|
+
- Upload the file directly to that URL via PUT (curl, code sandbox, etc.) — no base64
|
|
37
|
+
- Send the returned `path` to the other agent in your message
|
|
38
|
+
- get_attachment(path_or_url) → fetch and read a file another agent shared
|
|
39
|
+
|
|
40
|
+
## Deferred messages
|
|
41
|
+
|
|
42
|
+
reply(message_id, content, defer=true) sends a reply but keeps the original message visible in the inbox as "deferred". Use this when:
|
|
43
|
+
- The message needs attention from another agent or a later session
|
|
44
|
+
- You want to acknowledge receipt but can't fully handle it now
|
|
45
|
+
- The human says to mark/defer something for later
|
|
46
|
+
|
|
47
|
+
Deferred messages survive context compaction — the agent won't forget them.
|
|
48
|
+
|
|
49
|
+
## Other tools
|
|
50
|
+
|
|
51
|
+
- unsend_message(message_id) → unsend a message if recipient hasn't read it yet
|
|
52
|
+
- list_recent_debug(limit) → debug only, shows all recent messages including read ones
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
- Reply IMMEDIATELY. Never ask "want me to reply?" — just reply, then tell the human who wrote and what you answered.
|
|
57
|
+
- Only ask the human BEFORE replying if the request is destructive or requires secrets you don't have.
|
|
58
|
+
- Never ask "want me to wait?" — just wait
|
|
59
|
+
- Never show raw JSON to the human — summarize naturally
|
|
60
|
+
- One inbox() to orient. Don't call it repeatedly.
|
|
61
|
+
- If user says "check" or "check patchcord" — call inbox()
|
|
62
|
+
- Resolve machine names to agent_ids from inbox() results
|
|
63
|
+
- list_recent_debug is for debugging only — never call it routinely
|
|
64
|
+
- Do NOT reply to messages that don't need a response: acks, "ok", "noted", "seen", "👍", confirmations, thumbs up, "thanks", or anything that is clearly a conversation-ending signal. Just read them and move on. Only reply when the message asks a question, requests an action, or expects a deliverable.
|
|
65
|
+
- NEVER use `mcp__claude_ai_*` tools for patchcord. These are web interface OAuth tools with wrong identity. Always use `mcp__patchcord__*` (project-level). If only `claude_ai` tools are visible, diagnose the config: check `.mcp.json`, run `claude mcp get patchcord`, check `~/.claude/settings.json` deny rule.
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Auto-check patchcord inbox when idle. Enables persistent listening without a daemon.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"Stop": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/check-inbox.sh",
|
|
10
|
+
"timeout": 10
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"Notification": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "idle_prompt",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/check-inbox.sh",
|
|
22
|
+
"timeout": 10
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "patchcord",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Cross-machine agent messaging for Claude Code and Codex",
|
|
5
|
+
"author": "ppravdin",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/ppravdin/patchcord"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://patchcord.dev",
|
|
12
|
+
"keywords": ["claude-code", "codex", "mcp", "agent", "messaging", "plugin"],
|
|
13
|
+
"bin": {
|
|
14
|
+
"patchcord": "./bin/patchcord.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
".claude-plugin/",
|
|
19
|
+
"hooks/",
|
|
20
|
+
"scripts/",
|
|
21
|
+
"skills/",
|
|
22
|
+
"codex/",
|
|
23
|
+
"README.md"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
find_patchcord_mcp_json() {
|
|
5
|
+
local dir="${1:-$PWD}"
|
|
6
|
+
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
|
|
7
|
+
if [ -f "$dir/.mcp.json" ]; then
|
|
8
|
+
printf '%s\n' "$dir/.mcp.json"
|
|
9
|
+
return 0
|
|
10
|
+
fi
|
|
11
|
+
dir=$(dirname "$dir")
|
|
12
|
+
done
|
|
13
|
+
return 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
INPUT=$(cat)
|
|
17
|
+
|
|
18
|
+
# Guard against infinite loops: stop_hook_active is true when Claude
|
|
19
|
+
# is already continuing because a previous Stop hook told it to.
|
|
20
|
+
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
|
21
|
+
if [ "$STOP_ACTIVE" = "true" ]; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Resolve config from project-scoped .mcp.json only.
|
|
26
|
+
TOKEN=""
|
|
27
|
+
URL=""
|
|
28
|
+
MCP_JSON=$(find_patchcord_mcp_json "$PWD" || true)
|
|
29
|
+
|
|
30
|
+
if [ -n "$MCP_JSON" ]; then
|
|
31
|
+
MCP_URL=$(jq -r '.mcpServers.patchcord.url // empty' "$MCP_JSON" 2>/dev/null || true)
|
|
32
|
+
MCP_AUTH=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$MCP_JSON" 2>/dev/null || true)
|
|
33
|
+
if [ -n "$MCP_URL" ] && [ -n "$MCP_AUTH" ]; then
|
|
34
|
+
URL="${MCP_URL%/mcp}"
|
|
35
|
+
URL="${URL%/mcp/bearer}"
|
|
36
|
+
TOKEN="${MCP_AUTH#Bearer }"
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ -z "$URL" ] || [ -z "$TOKEN" ]; then
|
|
41
|
+
exit 0 # Not configured, skip silently
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Check inbox — one lightweight HTTP call
|
|
45
|
+
MACHINE_NAME=$(hostname -s 2>/dev/null || echo "unknown")
|
|
46
|
+
HTTP_CODE=$(curl -s -o /tmp/patchcord_inbox.json -w "%{http_code}" --max-time 5 \
|
|
47
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
48
|
+
-H "x-patchcord-machine: ${MACHINE_NAME}" \
|
|
49
|
+
"${URL}/api/inbox?status=pending&limit=1" 2>/dev/null || echo "000")
|
|
50
|
+
|
|
51
|
+
if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
|
|
52
|
+
jq -n '{
|
|
53
|
+
"decision": "block",
|
|
54
|
+
"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."
|
|
55
|
+
}'
|
|
56
|
+
rm -f /tmp/patchcord_inbox.json
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ "$HTTP_CODE" = "000" ]; then
|
|
61
|
+
# Server unreachable — skip silently
|
|
62
|
+
rm -f /tmp/patchcord_inbox.json
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
RESPONSE=$(cat /tmp/patchcord_inbox.json 2>/dev/null || echo '{"count":0}')
|
|
67
|
+
rm -f /tmp/patchcord_inbox.json
|
|
68
|
+
|
|
69
|
+
COUNT=$(echo "$RESPONSE" | jq -r '.count // .pending_count // 0' 2>/dev/null || echo "0")
|
|
70
|
+
|
|
71
|
+
if [ "$COUNT" -gt 0 ]; then
|
|
72
|
+
jq -n --arg count "$COUNT" '{
|
|
73
|
+
"decision": "block",
|
|
74
|
+
"reason": ($count + " patchcord message(s) waiting. Call inbox() and reply to all immediately.")
|
|
75
|
+
}'
|
|
76
|
+
else
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Enable the patchcord statusline in Claude Code user settings.
|
|
3
|
+
# Usage: bash enable-statusline.sh [--full]
|
|
4
|
+
# --full: also show model, context%, repo (branch)
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
EXTRA_ARGS=""
|
|
8
|
+
for arg in "$@"; do
|
|
9
|
+
[ "$arg" = "--full" ] && EXTRA_ARGS=" --full"
|
|
10
|
+
done
|
|
11
|
+
|
|
12
|
+
# Find the project root (walk up from cwd to find .mcp.json with patchcord)
|
|
13
|
+
find_project_root() {
|
|
14
|
+
local dir="${1:-$(pwd)}"
|
|
15
|
+
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
|
|
16
|
+
if [ -f "$dir/.mcp.json" ] && jq -e '.mcpServers.patchcord' "$dir/.mcp.json" >/dev/null 2>&1; then
|
|
17
|
+
printf '%s\n' "$dir"
|
|
18
|
+
return 0
|
|
19
|
+
fi
|
|
20
|
+
dir=$(dirname "$dir")
|
|
21
|
+
done
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
PROJECT_ROOT=$(find_project_root || true)
|
|
26
|
+
|
|
27
|
+
if [ -n "$PROJECT_ROOT" ]; then
|
|
28
|
+
# Project-level settings (only affects this repo)
|
|
29
|
+
SETTINGS="$PROJECT_ROOT/.claude/settings.json"
|
|
30
|
+
mkdir -p "$PROJECT_ROOT/.claude"
|
|
31
|
+
else
|
|
32
|
+
# Fallback to user-level if no project found
|
|
33
|
+
SETTINGS="$HOME/.claude/settings.json"
|
|
34
|
+
mkdir -p "$HOME/.claude"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Find the plugin's statusline script
|
|
38
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
39
|
+
STATUSLINE="$SCRIPT_DIR/statusline.sh"
|
|
40
|
+
|
|
41
|
+
if [ ! -f "$STATUSLINE" ]; then
|
|
42
|
+
echo "Error: statusline.sh not found at $STATUSLINE" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Read existing settings or start fresh
|
|
47
|
+
if [ -f "$SETTINGS" ]; then
|
|
48
|
+
CURRENT=$(cat "$SETTINGS")
|
|
49
|
+
else
|
|
50
|
+
CURRENT='{}'
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Set statusLine field
|
|
54
|
+
UPDATED=$(echo "$CURRENT" | jq --arg cmd "bash \"$STATUSLINE\"${EXTRA_ARGS}" '.statusLine = {"type": "command", "command": $cmd}')
|
|
55
|
+
echo "$UPDATED" > "$SETTINGS"
|
|
56
|
+
|
|
57
|
+
echo "Patchcord statusline enabled."
|
|
58
|
+
echo " Script: $STATUSLINE"
|
|
59
|
+
echo " Settings: $SETTINGS"
|
|
60
|
+
echo "Restart Claude Code to see the statusline."
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/bin/bash
|
|
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
|
+
#
|
|
10
|
+
# Receives session JSON on stdin, outputs ANSI-formatted text.
|
|
11
|
+
set -f
|
|
12
|
+
|
|
13
|
+
FULL=false
|
|
14
|
+
for arg in "$@"; do
|
|
15
|
+
[ "$arg" = "--full" ] && FULL=true
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
find_patchcord_mcp_json() {
|
|
19
|
+
local dir="$1"
|
|
20
|
+
while [ -n "$dir" ] && [ "$dir" != "/" ]; do
|
|
21
|
+
if [ -f "$dir/.mcp.json" ]; then
|
|
22
|
+
printf '%s\n' "$dir/.mcp.json"
|
|
23
|
+
return 0
|
|
24
|
+
fi
|
|
25
|
+
dir=$(dirname "$dir")
|
|
26
|
+
done
|
|
27
|
+
return 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
input=$(cat)
|
|
31
|
+
|
|
32
|
+
if [ -z "$input" ]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# ── Colors ──────────────────────────────────────────────
|
|
37
|
+
blue='\033[38;2;0;153;255m'
|
|
38
|
+
green='\033[38;2;0;175;80m'
|
|
39
|
+
cyan='\033[38;2;86;182;194m'
|
|
40
|
+
red='\033[38;2;255;85;85m'
|
|
41
|
+
yellow='\033[38;2;230;200;0m'
|
|
42
|
+
orange='\033[38;2;255;176;85m'
|
|
43
|
+
white='\033[38;2;220;220;220m'
|
|
44
|
+
dim='\033[2m'
|
|
45
|
+
reset='\033[0m'
|
|
46
|
+
|
|
47
|
+
sep=" ${dim}│${reset} "
|
|
48
|
+
|
|
49
|
+
# ── Patchcord: agent identity + inbox ───────────────────
|
|
50
|
+
cwd=$(echo "$input" | jq -r '.cwd // ""')
|
|
51
|
+
[ -z "$cwd" ] || [ "$cwd" = "null" ] && cwd=$(pwd)
|
|
52
|
+
|
|
53
|
+
pc_token=""
|
|
54
|
+
pc_url=""
|
|
55
|
+
mcp_json=$(find_patchcord_mcp_json "$cwd" || true)
|
|
56
|
+
|
|
57
|
+
if [ -n "$mcp_json" ]; then
|
|
58
|
+
mcp_url=$(jq -r '.mcpServers.patchcord.url // empty' "$mcp_json" 2>/dev/null || true)
|
|
59
|
+
mcp_auth=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$mcp_json" 2>/dev/null || true)
|
|
60
|
+
if [ -n "$mcp_url" ] && [ -n "$mcp_auth" ]; then
|
|
61
|
+
pc_url="${mcp_url%/mcp}"
|
|
62
|
+
pc_token="${mcp_auth#Bearer }"
|
|
63
|
+
fi
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
pc_part=""
|
|
67
|
+
if [ -n "$pc_url" ] && [ -n "$pc_token" ]; then
|
|
68
|
+
cache_key=$(printf '%s\n%s\n' "$mcp_json" "$pc_url" | sha256sum | awk '{print $1}')
|
|
69
|
+
cache_file="/tmp/claude/patchcord-statusline-${cache_key}.json"
|
|
70
|
+
cache_max_age=20
|
|
71
|
+
mkdir -p /tmp/claude
|
|
72
|
+
|
|
73
|
+
needs_refresh=true
|
|
74
|
+
pc_data=""
|
|
75
|
+
|
|
76
|
+
if [ -f "$cache_file" ]; then
|
|
77
|
+
cache_mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null)
|
|
78
|
+
now=$(date +%s)
|
|
79
|
+
if [ $(( now - cache_mtime )) -lt $cache_max_age ]; then
|
|
80
|
+
needs_refresh=false
|
|
81
|
+
pc_data=$(cat "$cache_file" 2>/dev/null)
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if $needs_refresh; then
|
|
86
|
+
http_code=$(curl -s -o /tmp/claude/patchcord-sl-resp.json -w "%{http_code}" --max-time 3 \
|
|
87
|
+
-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"
|
|
95
|
+
fi
|
|
96
|
+
rm -f /tmp/claude/patchcord-sl-resp.json
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
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
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# No patchcord config — output nothing in default mode
|
|
130
|
+
if [ -z "$pc_part" ] && ! $FULL; then
|
|
131
|
+
exit 0
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# ── Build line ──────────────────────────────────────────
|
|
135
|
+
line=""
|
|
136
|
+
|
|
137
|
+
if $FULL; then
|
|
138
|
+
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
139
|
+
|
|
140
|
+
size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
|
|
141
|
+
[ "$size" -eq 0 ] 2>/dev/null && size=200000
|
|
142
|
+
input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
|
|
143
|
+
cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
|
|
144
|
+
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
|
|
145
|
+
current=$(( input_tokens + cache_create + cache_read ))
|
|
146
|
+
if [ "$size" -gt 0 ]; then
|
|
147
|
+
pct_used=$(( current * 100 / size ))
|
|
148
|
+
else
|
|
149
|
+
pct_used=0
|
|
150
|
+
fi
|
|
151
|
+
if [ "$pct_used" -ge 90 ]; then pct_color="$red"
|
|
152
|
+
elif [ "$pct_used" -ge 70 ]; then pct_color="$yellow"
|
|
153
|
+
elif [ "$pct_used" -ge 50 ]; then pct_color="$orange"
|
|
154
|
+
else pct_color="$green"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
dirname=$(basename "$cwd")
|
|
158
|
+
git_branch=""
|
|
159
|
+
git_dirty=""
|
|
160
|
+
if git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
161
|
+
git_branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null)
|
|
162
|
+
if [ -n "$(git -C "$cwd" status --porcelain 2>/dev/null)" ]; then
|
|
163
|
+
git_dirty="*"
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
line="${blue}${model_name}${reset}"
|
|
168
|
+
line+="${sep}"
|
|
169
|
+
line+="${pct_color}${pct_used}%${reset}"
|
|
170
|
+
line+="${sep}"
|
|
171
|
+
line+="${cyan}${dirname}${reset}"
|
|
172
|
+
if [ -n "$git_branch" ]; then
|
|
173
|
+
line+=" ${green}(${git_branch}${red}${git_dirty}${green})${reset}"
|
|
174
|
+
fi
|
|
175
|
+
if [ -n "$pc_part" ]; then
|
|
176
|
+
line+="${sep}"
|
|
177
|
+
line+="${pc_part}"
|
|
178
|
+
fi
|
|
179
|
+
else
|
|
180
|
+
line="${pc_part}"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# ── Output ──────────────────────────────────────────────
|
|
184
|
+
printf "%b" "$line"
|
|
185
|
+
exit 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: patchcord
|
|
3
|
+
description: >
|
|
4
|
+
Cross-agent messaging across MCP-connected agents. Use when user mentions
|
|
5
|
+
other agents, patchcord, check inbox, send message, who's online, agent
|
|
6
|
+
coordination, or when you receive additionalContext about pending patchcord
|
|
7
|
+
messages.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# patchcord
|
|
11
|
+
|
|
12
|
+
9 MCP tools: inbox, send_message, reply, unsend_message, wait_for_message, upload_attachment, get_attachment, relay_url, list_recent_debug.
|
|
13
|
+
|
|
14
|
+
## On session start or when prompted by a hook
|
|
15
|
+
|
|
16
|
+
Call inbox(). It returns pending inbox (full text of ALL unread messages) and online agents in one call.
|
|
17
|
+
If there are pending messages, reply to ALL of them IMMEDIATELY. Do not ask the human first. Do not explain what you plan to reply. Just reply, then tell the human what you received and what you answered.
|
|
18
|
+
|
|
19
|
+
## Sending
|
|
20
|
+
|
|
21
|
+
1. inbox() — check who's online
|
|
22
|
+
2. send_message("agent_name", "specific question with file paths and context")
|
|
23
|
+
3. wait_for_message() — auto-wait for any response, don't ask human whether to wait
|
|
24
|
+
|
|
25
|
+
## Receiving (inbox has messages)
|
|
26
|
+
|
|
27
|
+
1. Read the question from inbox() result
|
|
28
|
+
2. Answer from YOUR project's actual code — reference real files, real lines
|
|
29
|
+
3. reply(message_id, "detailed answer")
|
|
30
|
+
4. wait_for_message() — stay responsive for follow-ups
|
|
31
|
+
5. If you can't answer, say so. Don't guess about another agent's code.
|
|
32
|
+
|
|
33
|
+
## File sharing
|
|
34
|
+
|
|
35
|
+
- upload_attachment(filename, mime_type) → returns presigned upload URL
|
|
36
|
+
- Upload the file directly to that URL via PUT (curl, code sandbox, etc.) — no base64
|
|
37
|
+
- Send the returned `path` to the other agent in your message
|
|
38
|
+
- get_attachment(path_or_url) → fetch and read a file another agent shared
|
|
39
|
+
|
|
40
|
+
## Deferred messages
|
|
41
|
+
|
|
42
|
+
reply(message_id, content, defer=true) sends a reply but keeps the original message visible in the inbox as "deferred". Use this when:
|
|
43
|
+
- The message needs attention from another agent or a later session
|
|
44
|
+
- You want to acknowledge receipt but can't fully handle it now
|
|
45
|
+
- The human says to mark/defer something for later
|
|
46
|
+
|
|
47
|
+
Deferred messages survive context compaction — the agent won't forget them.
|
|
48
|
+
|
|
49
|
+
## Other tools
|
|
50
|
+
|
|
51
|
+
- unsend_message(message_id) → unsend a message if recipient hasn't read it yet
|
|
52
|
+
- list_recent_debug(limit) → debug only, shows all recent messages including read ones
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
- Reply IMMEDIATELY. Never ask "want me to reply?" — just reply, then tell the human who wrote and what you answered.
|
|
57
|
+
- Only ask the human BEFORE replying if the request is destructive or requires secrets you don't have.
|
|
58
|
+
- Never ask "want me to wait?" — just wait
|
|
59
|
+
- Never show raw JSON to the human — summarize naturally
|
|
60
|
+
- One inbox() to orient. Don't call it repeatedly.
|
|
61
|
+
- If user says "check" or "check patchcord" — call inbox()
|
|
62
|
+
- Resolve machine names to agent_ids from inbox() results
|
|
63
|
+
- list_recent_debug is for debugging only — never call it routinely
|
|
64
|
+
- Do NOT reply to messages that don't need a response: acks, "ok", "noted", "seen", "👍", confirmations, thumbs up, "thanks", or anything that is clearly a conversation-ending signal. Just read them and move on. Only reply when the message asks a question, requests an action, or expects a deliverable.
|
|
65
|
+
- NEVER use `mcp__claude_ai_*` tools for patchcord. These are web interface OAuth tools with wrong identity. Always use `mcp__patchcord__*` (project-level). If only `claude_ai` tools are visible, diagnose the config: check `.mcp.json`, run `claude mcp get patchcord`, check `~/.claude/settings.json` deny rule.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "patchcord:wait"
|
|
3
|
+
description: >
|
|
4
|
+
Enter listening mode — wait for incoming patchcord messages. Use when user
|
|
5
|
+
says "wait", "listen", "stand by", or wants the agent to stay responsive
|
|
6
|
+
to other agents.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# patchcord:wait
|
|
10
|
+
|
|
11
|
+
Enter listening mode. Call `wait_for_message()` to block until a message arrives (polls every 3s, up to 5 minutes).
|
|
12
|
+
|
|
13
|
+
When a message arrives:
|
|
14
|
+
1. Read it — the tool returns from, content, and message_id
|
|
15
|
+
2. Reply immediately: `reply(message_id, "your answer")`
|
|
16
|
+
3. Tell the human who wrote and what you answered
|
|
17
|
+
4. Call `wait_for_message()` again to keep listening
|
|
18
|
+
|
|
19
|
+
Loop until timeout or the human interrupts.
|
|
20
|
+
|
|
21
|
+
Do NOT ask the human for permission to reply — just reply, then report.
|