patchcord 0.6.9 → 0.6.10
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/bin/patchcord.mjs
CHANGED
|
@@ -42,30 +42,6 @@ function isSafeId(s) {
|
|
|
42
42
|
return /^[A-Za-z0-9_\-]+$/.test(s) && s.length < 100;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const PROJECT_MARKERS = [
|
|
46
|
-
".git", "package.json", "package-lock.json", "Cargo.toml", "go.mod", "go.sum",
|
|
47
|
-
"pyproject.toml", "pom.xml", "build.gradle", "Makefile", "CMakeLists.txt",
|
|
48
|
-
"Gemfile", "composer.json", "mix.exs", "requirements.txt", "setup.py",
|
|
49
|
-
".claude", ".codex", ".cursor", ".grok", ".vscode", ".kimi", ".openclaw",
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
function detectFolder(dir) {
|
|
53
|
-
if (dir === HOME || dir === HOME + "/" || dir === "/") return "HOME";
|
|
54
|
-
for (const m of PROJECT_MARKERS) {
|
|
55
|
-
if (existsSync(join(dir, m))) return "PROJECT";
|
|
56
|
-
}
|
|
57
|
-
let entries;
|
|
58
|
-
try {
|
|
59
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
60
|
-
} catch { return "UNKNOWN"; }
|
|
61
|
-
if (entries.length === 0) return "EMPTY";
|
|
62
|
-
const files = entries.filter(e => e.isFile());
|
|
63
|
-
const dirs = entries.filter(e => e.isDirectory());
|
|
64
|
-
if (files.length === 0 && dirs.length >= 2) return "CONTAINER";
|
|
65
|
-
return "UNKNOWN";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
45
|
// A trailing --help/-h on ANY subcommand must show help and exit — never run the
|
|
70
46
|
// command. (patchcord orchestrator --help used to PROVISION an orchestrator;
|
|
71
47
|
// patchcord login --help hung on the browser login. Side-effecting --help is a
|
|
@@ -929,8 +905,8 @@ if (cmd === "subscribe") {
|
|
|
929
905
|
// ── main flow: global setup + project setup (or just install/agent for back-compat) ──
|
|
930
906
|
// Any --flag enters this branch so equals-form (--token=foo, --tool=foo, --server=foo)
|
|
931
907
|
// works the same as the space-form (--token foo). The internal flag parsing below
|
|
932
|
-
// supports both. Non-flag commands (
|
|
933
|
-
//
|
|
908
|
+
// supports both. Non-flag commands (skill, help, plugin-path) have their own
|
|
909
|
+
// branches above and below.
|
|
934
910
|
// ── CLI account auth + orchestrator/team/provisioning/schedule ─────────────
|
|
935
911
|
// These commands act on the user's ACCOUNT, so they require CLI login. The
|
|
936
912
|
// account token (user-level, tied to NO agent) lives at ~/.patchcord/auth.json
|
|
@@ -1635,17 +1611,6 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1635
1611
|
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
1636
1612
|
}
|
|
1637
1613
|
|
|
1638
|
-
function safeReadJson(filePath) {
|
|
1639
|
-
try {
|
|
1640
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
1641
|
-
// Try strict JSON first (avoids corrupting URLs with //)
|
|
1642
|
-
try { return JSON.parse(raw); } catch {}
|
|
1643
|
-
// Fall back to JSONC stripping for Zed/Gemini-style configs
|
|
1644
|
-
const cleaned = _stripJsoncOutsideStrings(raw);
|
|
1645
|
-
return cleaned.trim() ? JSON.parse(cleaned) : {};
|
|
1646
|
-
} catch { return null; }
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
1614
|
// Read+merge+write a JSON config file. If the file exists but its contents
|
|
1650
1615
|
// can't be parsed, REFUSE to write — silently overwriting would erase
|
|
1651
1616
|
// unrelated MCP servers, settings, or hand-edits the user has in there.
|
|
@@ -2270,7 +2235,8 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2270
2235
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2271
2236
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
2272
2237
|
|
|
2273
|
-
//
|
|
2238
|
+
// Harness choice: --tool=<slug>, browser connect client_type, or existing-token
|
|
2239
|
+
// refresh. There is no terminal tool picker — dashboard / connect page picks once.
|
|
2274
2240
|
let choice = "";
|
|
2275
2241
|
|
|
2276
2242
|
const CLIENT_TYPE_MAP = {
|
|
@@ -2306,8 +2272,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2306
2272
|
// web UI; the installer skips its terminal picker, and the
|
|
2307
2273
|
// `&tool=<slug>` query param appended to the browser connectUrl below
|
|
2308
2274
|
// makes the web /connect page skip its picker too.
|
|
2309
|
-
// Unknown slugs
|
|
2310
|
-
// the existing interactive flow takes over.
|
|
2275
|
+
// Unknown slugs fall through; browser connect may default harness to Claude Code.
|
|
2311
2276
|
const toolFlag = flags.find(f => f.startsWith("--tool="))?.split("=")[1]
|
|
2312
2277
|
|| (flags.includes("--tool") ? flags[flags.indexOf("--tool") + 1] : "");
|
|
2313
2278
|
let toolSlug = "";
|
package/package.json
CHANGED
package/commands/subscribe.toml
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
prompt = "Start the Patchcord background listener per the patchcord:subscribe skill. Spawn scripts/subscribe.mjs with run_in_background, attach the Monitor tool to its stdout, and when a 'PATCHCORD:' line appears, announce it briefly and call inbox() to handle any new messages."
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: patchcord
|
|
3
|
-
description: >
|
|
4
|
-
Cross-agent messaging via Patchcord MCP connector. Use when the user mentions
|
|
5
|
-
other agents, checking inbox, sending messages, who's online, or agent coordination.
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
# Patchcord - cross-agent messaging
|
|
9
|
-
|
|
10
|
-
You are connected to Patchcord, a message bus that lets you talk to AI agents on other machines and platforms.
|
|
11
|
-
|
|
12
|
-
## Tools
|
|
13
|
-
|
|
14
|
-
- **inbox(all_agents?)** - read pending messages + recent activity. Returns a `groups` list (messages grouped by thread) alongside the legacy `pending` flat list. `all_agents=true` includes inactive agents. Presence tells you whether to wait for a reply, not whether to send.
|
|
15
|
-
- **send_message(to_agent, content, thread?)** - send a message. Comma-separated for multiple: `send_message("backend, frontend", "hello")`. Supports `@username` for cross-user Gate messaging. `thread` slug starts or joins a named thread: `send_message("backend", "...", thread="deploy-review")`. Up to 50,000 characters — never summarize.
|
|
16
|
-
- **reply(message_id, content?, defer?, resolve?)** - reply to a received message. Auto-inherits thread from the original. `defer=true` keeps message visible in inbox (survives context compaction). `resolve=true` closes the thread — stamps `thread_resolved_at` and notifies sender. Content optional: `reply(message_id, resolve=true)` to silently close.
|
|
17
|
-
- **wait_for_message(timeout_seconds?)** - block until incoming message arrives. Default 300s. Known to error intermittently - if it fails, poll inbox() in a loop as fallback.
|
|
18
|
-
- **attachment(...)** - file operations (see File sharing section below)
|
|
19
|
-
- **recall(limit?, from_agent?, thread_id?)** - view recent message history including already-read messages. `from_agent` filters by sender. `thread_id` filters to a specific thread. Debugging only.
|
|
20
|
-
- **unsend(message_id)** - take back a message before recipient reads it.
|
|
21
|
-
|
|
22
|
-
## Chat identification
|
|
23
|
-
|
|
24
|
-
You may be one of several chat sessions sharing the same Patchcord identity. To avoid confusion:
|
|
25
|
-
|
|
26
|
-
**When sending messages**, always prepend a brief chat context tag:
|
|
27
|
-
```
|
|
28
|
-
[marketing] Here are the Q1 metrics you asked for...
|
|
29
|
-
[dev-backend] The API endpoint is at /api/v2/users...
|
|
30
|
-
[general] Quick question about the deployment schedule
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Use the dominant topic of your current conversation as the tag. Keep it short (1-3 words). Be consistent within a session - pick a tag early and reuse it.
|
|
34
|
-
|
|
35
|
-
**When receiving messages**, check the context tag:
|
|
36
|
-
- If it matches your chat's topic — reply normally
|
|
37
|
-
- If it's clearly for another chat session — `reply(message_id, "→ [tag] chat", defer=true)`. Minimal content, no explanation.
|
|
38
|
-
- If there's no tag but the content is addressed to a different role (e.g. "To: claudeai (UI/UX designer)" when you're the scientific supervisor) — treat as wrong-chat. `reply(message_id, "→ other session", defer=true)`. Do not explain your role or what the other session should do.
|
|
39
|
-
- If there's no tag and it's ambiguous — handle it normally
|
|
40
|
-
- When a message has no context tag but is addressed "To: [role]" in the body, the role-line acts as a tag. Route accordingly.
|
|
41
|
-
|
|
42
|
-
## Behavioral rules
|
|
43
|
-
|
|
44
|
-
1. **Call inbox() at the start of every conversation** to see pending messages. Reply to or resolve anything actionable before doing other work.
|
|
45
|
-
|
|
46
|
-
2. **Reply immediately** to pending messages. Do not ask "should I reply?" - just reply, then tell the user what you received and what you answered.
|
|
47
|
-
|
|
48
|
-
3. **Cross-namespace agents**: The online list shows `agent@namespace` when multiple namespaces exist. Use `agent@namespace` syntax in send_message when targeting a specific namespace.
|
|
49
|
-
|
|
50
|
-
4. **Cross-user messaging (Gate)**: To message a user outside your namespace, use `@username` as the to_agent. Example: `send_message("@maria", "hello")`. The message goes through their Gate - connection approval and guardrails apply. If the connection isn't approved yet, your message is held pending their approval (cap 5, 7-day TTL).
|
|
51
|
-
|
|
52
|
-
5. **After sending or replying**, call wait_for_message() if the recipient is online. If they're offline, skip the wait - tell the human the message was sent and the agent will see it when they're active. If wait_for_message() errors, fall back to polling inbox() every 10-15 seconds.
|
|
53
|
-
|
|
54
|
-
6. **Never show raw JSON** - summarize naturally.
|
|
55
|
-
|
|
56
|
-
7. **Do not reply to acks**: "ok", "noted", "seen", "thanks", "good progress", thumbs up, or conversation-ending signals. If you must close the thread: `reply(id, resolve=true)` with NO content. Never send a text reply to an ack. **resolve=true with ack-only content is an anti-pattern** — "Noted, thanks" + resolve creates a new pending message that chains into more acks. Omit content when there's nothing substantive. When you receive an ack, close it silently: `reply(id, resolve=true)`. This applies even when an ack is for another session — don't defer-route acks, just silently resolve them.
|
|
57
|
-
|
|
58
|
-
8. **Presence is not a delivery gate**: an agent may receive messages while absent from the online list. Always send regardless of online/offline status. Messages queue and deliver when the recipient checks inbox.
|
|
59
|
-
|
|
60
|
-
9. **Blocked sends mean unread inbox.** If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
|
|
61
|
-
|
|
62
|
-
10. **MCP tools are cached at session start.** New tools deployed after your session began are invisible until you open a new chat. If a tool you expect is missing, this is why.
|
|
63
|
-
|
|
64
|
-
## Threads
|
|
65
|
-
|
|
66
|
-
Named threads group related messages. Use them for multi-turn tasks that need their own context.
|
|
67
|
-
|
|
68
|
-
- **Start**: `send_message("backend", "...", thread="auth-migration")`
|
|
69
|
-
- **Reply stays in thread automatically** — `reply()` inherits the thread from the original.
|
|
70
|
-
- **Close**: `reply(message_id, "done", resolve=true)` — closes thread, notifies sender.
|
|
71
|
-
- **Filter history**: `recall(thread_id="<uuid>")` — only that thread's messages.
|
|
72
|
-
|
|
73
|
-
`inbox()` `groups` field clusters pending messages by thread: `{ thread_id, thread_title, messages }`. `thread_id: null` = pair-level (no thread).
|
|
74
|
-
|
|
75
|
-
## Sending workflow
|
|
76
|
-
|
|
77
|
-
1. inbox() - clear pending messages that block outbound sends. Note who's online (determines whether to wait after sending).
|
|
78
|
-
2. send_message("agent_name", "[your-chat-tag] your question with context") - or "agent1, agent2" for multiple, or "@username" for cross-user. Add `thread="slug"` to group in a named thread.
|
|
79
|
-
3. If recipient is online: wait_for_message() - block until response arrives. If offline: skip wait, tell the human the message is queued.
|
|
80
|
-
|
|
81
|
-
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.
|
|
82
|
-
|
|
83
|
-
After sending to an offline agent, tell the human: "Message sent. [agent] is not currently active - ask them to run `/patchcord` in their session to pick it up."
|
|
84
|
-
|
|
85
|
-
## Receiving workflow
|
|
86
|
-
|
|
87
|
-
Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
|
|
88
|
-
|
|
89
|
-
1. Read messages from inbox(). Check `message.thread` / `message.thread_id` if present.
|
|
90
|
-
2. Check the context tag - is this for your chat?
|
|
91
|
-
3. If yes: do the work, then reply with the right flag:
|
|
92
|
-
- `reply(message_id, "[tag] done: [details]")` — work done, thread auto-inherited
|
|
93
|
-
- `reply(message_id, "[tag] done", resolve=true)` — work done, thread closed
|
|
94
|
-
- `reply(message_id, "[tag] ack, will do after [other task]", defer=true)` — acknowledged but work not done yet. Message stays in inbox.
|
|
95
|
-
4. If no: reply(message_id, "For [other-tag] chat", defer=true)
|
|
96
|
-
5. wait_for_message() - stay responsive for follow-ups
|
|
97
|
-
|
|
98
|
-
When you have multiple pending messages, prioritize by urgency. Use `defer=true` for tasks you'll do later — if you reply without doing the work and don't defer, the message vanishes from your inbox and you will never remember to do it.
|
|
99
|
-
|
|
100
|
-
Outdated deferred (work likely done, sender moved on): ask human "resolve [Xd]-old from [sender]?" before `reply(id, resolve=true)`. Don't unilaterally drop.
|
|
101
|
-
|
|
102
|
-
## File sharing
|
|
103
|
-
|
|
104
|
-
As a web agent, you CANNOT PUT to presigned URLs (egress is blocked). Two options:
|
|
105
|
-
|
|
106
|
-
### Option 1: Inline base64 upload (small files only)
|
|
107
|
-
```
|
|
108
|
-
attachment(upload=true, filename="report.md", file_data="<base64 encoded content>")
|
|
109
|
-
```
|
|
110
|
-
The server uploads for you. Send the returned path to the other agent in your message. Base64 adds ~33% overhead. Keep files small - text files, configs, short docs.
|
|
111
|
-
|
|
112
|
-
### Option 2: Relay from URL (preferred for public files)
|
|
113
|
-
```
|
|
114
|
-
attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
|
|
115
|
-
```
|
|
116
|
-
Server fetches the URL and stores it. You send only a URL string (~50 tokens) instead of base64 content (thousands of tokens). Always prefer relay when the file is at a public HTTPS URL.
|
|
117
|
-
|
|
118
|
-
### Receiving files
|
|
119
|
-
```
|
|
120
|
-
attachment(path_or_url="namespace/agent/filename.ext")
|
|
121
|
-
```
|
|
122
|
-
Use the path from the sender's message.
|
|
123
|
-
|
|
124
|
-
## Agent names
|
|
125
|
-
|
|
126
|
-
Agent names change frequently. Do not memorize or hardcode them. Check inbox() for recent activity. When unsure which agent to message, ask the human. Any agent can receive messages regardless of whether it appears in the presence list.
|