patchcord 0.5.28 → 0.5.30

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
@@ -67,6 +67,7 @@ Usage:
67
67
  npx patchcord@latest --token <token> Self-hosted / CI setup
68
68
  npx patchcord@latest --token <token> --server <url> Self-hosted with custom server
69
69
  npx patchcord@latest --full Same + full statusline
70
+ npx patchcord@latest --rename <new-name> Rename this agent (paste from dashboard)
70
71
  npx patchcord@latest skill apply Fetch custom skill from web console`);
71
72
  process.exit(0);
72
73
  }
@@ -76,6 +77,123 @@ if (cmd === "plugin-path") {
76
77
  process.exit(0);
77
78
  }
78
79
 
80
+ // ── --rename <new> [--expect-token <prefix>] ────────────────────
81
+ // Renames the agent in cwd's .mcp.json. Dashboard generates the full
82
+ // pasted command including --expect-token to prevent wrong-machine paste.
83
+ {
84
+ const renameIdx = process.argv.indexOf("--rename");
85
+ if (renameIdx !== -1) {
86
+ const newName = (process.argv[renameIdx + 1] || "").trim().toLowerCase();
87
+ const expectIdx = process.argv.indexOf("--expect-token");
88
+ const expectPrefix = expectIdx !== -1 ? (process.argv[expectIdx + 1] || "").trim() : "";
89
+
90
+ if (!newName || !/^[a-z][a-z0-9-]{1,49}$/.test(newName)) {
91
+ console.error("Usage: npx patchcord@latest --rename <new-name> [--expect-token <prefix>]");
92
+ console.error(" <new-name> must be lowercase letters, digits, dashes; 2-50 chars");
93
+ process.exit(1);
94
+ }
95
+
96
+ const cwd = process.cwd();
97
+ let mcpJson = null;
98
+ let dir = cwd;
99
+ while (dir && dir !== "/") {
100
+ const p = join(dir, ".mcp.json");
101
+ if (existsSync(p)) { mcpJson = p; break; }
102
+ dir = dirname(dir);
103
+ }
104
+ if (!mcpJson) {
105
+ console.error(`No .mcp.json found above ${cwd}. Run from your patchcord project folder.`);
106
+ process.exit(1);
107
+ }
108
+
109
+ const { readFileSync } = await import("fs");
110
+ let config;
111
+ try {
112
+ config = JSON.parse(readFileSync(mcpJson, "utf-8"));
113
+ } catch (e) {
114
+ console.error(`Cannot parse ${mcpJson}: ${e.message}`);
115
+ process.exit(1);
116
+ }
117
+ const mcpUrl = config?.mcpServers?.patchcord?.url || "";
118
+ const auth = config?.mcpServers?.patchcord?.headers?.Authorization || "";
119
+ const baseUrl = mcpUrl.replace(/\/mcp(\/bearer)?$/, "");
120
+ const token = auth.replace(/^Bearer\s+/, "");
121
+
122
+ if (!baseUrl || !token || !isSafeToken(token) || !isSafeUrl(baseUrl)) {
123
+ console.error(`Cannot read patchcord URL/token from ${mcpJson}.`);
124
+ process.exit(1);
125
+ }
126
+
127
+ if (expectPrefix && !token.startsWith(expectPrefix)) {
128
+ console.error("✗ Token mismatch.");
129
+ console.error(` The dashboard issued this rename for a different agent than what's`);
130
+ console.error(` installed in ${dirname(mcpJson)}.`);
131
+ console.error(` Maybe you clicked Rename while looking at a different machine?`);
132
+ process.exit(2);
133
+ }
134
+
135
+ // Call the server. Use curl + -w for status code so we can distinguish
136
+ // 409 (taken) from generic 500.
137
+ const tmpResp = `/tmp/patchcord-rename-${process.pid}.json`;
138
+ const status = run(
139
+ `curl -s -o "${tmpResp}" -w "%{http_code}" -X POST ` +
140
+ `-H "Authorization: Bearer ${token}" ` +
141
+ `-H "Content-Type: application/json" ` +
142
+ `-d '{"new":"${newName}"}' ` +
143
+ `"${baseUrl}/api/agent/rename"`
144
+ );
145
+ let respBody = "";
146
+ try { respBody = readFileSync(tmpResp, "utf-8"); } catch {}
147
+ try { (await import("fs")).unlinkSync(tmpResp); } catch {}
148
+
149
+ if (status !== "200") {
150
+ let msg = "rename failed";
151
+ try { msg = (JSON.parse(respBody).error || msg); } catch {}
152
+ console.error(`✗ HTTP ${status}: ${msg}`);
153
+ process.exit(1);
154
+ }
155
+
156
+ let payload = {};
157
+ try { payload = JSON.parse(respBody); } catch {}
158
+ const oldName = payload.old_agent_id || "";
159
+ const ns = payload.namespace_id || "";
160
+ const aliasUntil = payload.alias_expires_at || "";
161
+
162
+ // Cache wipe + subscribe kill (best-effort; failures non-fatal).
163
+ const { unlinkSync, readdirSync: rd } = await import("fs");
164
+ const tmpClaude = "/tmp/claude";
165
+ if (existsSync(tmpClaude)) {
166
+ try {
167
+ for (const f of rd(tmpClaude)) {
168
+ if (f.startsWith("patchcord-statusline-") || f === "patchcord-sl-resp.json") {
169
+ try { unlinkSync(join(tmpClaude, f)); } catch {}
170
+ }
171
+ }
172
+ } catch {}
173
+ }
174
+ if (ns && oldName) {
175
+ const pidfile = `/tmp/patchcord_subscribe_${ns}_${oldName}.pid`;
176
+ if (existsSync(pidfile)) {
177
+ try {
178
+ const pid = Number(readFileSync(pidfile, "utf-8").trim());
179
+ if (pid > 0) {
180
+ try { process.kill(pid, "SIGTERM"); } catch {}
181
+ }
182
+ } catch {}
183
+ try { unlinkSync(pidfile); } catch {}
184
+ }
185
+ }
186
+
187
+ const ttlNote = aliasUntil
188
+ ? ` Senders using "${oldName}" will be redirected until ${aliasUntil.slice(0, 10)}.`
189
+ : "";
190
+ console.log(`✓ Renamed: ${oldName} → ${newName}${ttlNote}`);
191
+ console.log(` Restart open Claude Code / Codex sessions for this agent to pick up the new name.`);
192
+ console.log(` If you were running /patchcord:subscribe, run it again to reconnect.`);
193
+ process.exit(0);
194
+ }
195
+ }
196
+
79
197
  // ── main flow: global setup + project setup (or just install/agent for back-compat) ──
80
198
  // Any --flag enters this branch so equals-form (--token=foo, --tool=foo, --server=foo)
81
199
  // works the same as the space-form (--token foo). The internal flag parsing below
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.28",
3
+ "version": "0.5.30",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -74,7 +74,7 @@ If send_message fails with a send gate error: call inbox(), reply to or resolve
74
74
 
75
75
  ## Receiving workflow
76
76
 
77
- **Check the age first.** Every pending message shows a relative timestamp like `(3h ago)` or `(45d ago)`. For action requests older than 7 days, **do not execute** — ask the human first. Old tasks are usually already done, superseded, or no longer wanted. Ack-style messages and pure FYIs can still be silently resolved at any age.
77
+ Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
78
78
 
79
79
  1. Read the message from `inbox()` or `wait_for_message()`. Check `message.thread` / `message.thread_id` if present.
80
80
  2. Do the work - use real code, real files, real results from your project
@@ -87,7 +87,7 @@ If send_message fails with a send gate error: call inbox(), reply to or resolve
87
87
 
88
88
  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.
89
89
 
90
- **Prune stale deferred messages.** Deferred messages persist across sessions and consume context on every inbox(). If you see a deferred message that looks outdated (old timestamp, work probably already done, requirements likely changed, sender moved on), ask the human: *"This deferred message from [sender] is [Xd] old — should I resolve it?"* On confirmation, `reply(id, resolve=true)` clears it. Don't unilaterally resolve a deferred task — but don't let dead weight accumulate either.
90
+ Outdated deferred (work likely done, sender moved on): ask human "resolve [Xd]-old from [sender]?" before `reply(id, resolve=true)`. Don't unilaterally drop.
91
91
 
92
92
  ## Cross-user messaging (Gate)
93
93
 
@@ -84,7 +84,7 @@ After sending to an offline agent, tell the human: "Message sent. [agent] is not
84
84
 
85
85
  ## Receiving workflow
86
86
 
87
- **Check the age first.** Every pending message shows a relative timestamp like `(3h ago)` or `(45d ago)`. For action requests older than 7 days, **do not execute** — ask the human first. Old tasks are usually already done, superseded, or no longer wanted. Ack-style messages and pure FYIs can still be silently resolved at any age.
87
+ Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
88
88
 
89
89
  1. Read messages from inbox(). Check `message.thread` / `message.thread_id` if present.
90
90
  2. Check the context tag - is this for your chat?
@@ -97,7 +97,7 @@ After sending to an offline agent, tell the human: "Message sent. [agent] is not
97
97
 
98
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
99
 
100
- **Prune stale deferred messages.** Deferred messages persist across sessions and consume context on every inbox(). If you see a deferred message that looks outdated (old timestamp, work probably already done, requirements likely changed, sender moved on), ask the human: *"This deferred message from [sender] is [Xd] old — should I resolve it?"* On confirmation, `reply(id, resolve=true)` clears it. Don't unilaterally resolve a deferred task — but don't let dead weight accumulate either.
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
101
 
102
102
  ## File sharing
103
103
 
@@ -61,7 +61,7 @@ If send_message fails with a send gate error: call inbox(), reply to or resolve
61
61
 
62
62
  ## Receiving (inbox has messages)
63
63
 
64
- **Check the age first.** Every pending message shows a relative timestamp like `(3h ago)` or `(45d ago)`. For action requests older than 7 days, **do not execute the task** — ask the human first. Old tasks are usually already done, superseded, or no longer wanted. Confirmation cost (one short question) is far less than cost of running a stale plan. Ack-style messages and pure FYIs can still be silently resolved at any age.
64
+ Action requests older than 7d (per the `(Xd ago)` stamp): ask human before executing. Acks/FYIs silent-resolve at any age.
65
65
 
66
66
  1. Read the message. If it belongs to a thread, `message.thread` and `message.thread_id` will be present.
67
67
  2. Do the work described in the message - using your project's actual code, real files, real lines
@@ -75,7 +75,7 @@ If send_message fails with a send gate error: call inbox(), reply to or resolve
75
75
 
76
76
  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.
77
77
 
78
- **Prune stale deferred messages.** Deferred messages persist across sessions and consume context on every inbox(). If you see a deferred message that looks outdated (old timestamp, work probably already done elsewhere, requirements likely changed, sender moved on), ask the human: *"This deferred message from [sender] is [Xd] old — should I resolve it?"* On confirmation, `reply(id, resolve=true)` clears it. Don't unilaterally resolve a deferred task — but don't let dead weight accumulate either.
78
+ Outdated deferred (work likely done, sender moved on): ask human "resolve [Xd]-old from [sender]?" before `reply(id, resolve=true)`. Don't unilaterally drop.
79
79
 
80
80
  ## Cross-user messaging (Gate)
81
81