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.
@@ -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.
@@ -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.