patchcord 0.3.98 → 0.4.0

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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { existsSync, mkdirSync, cpSync, readdirSync } from "fs";
4
- import { join, dirname } from "path";
4
+ import { join, dirname, basename } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { execSync } from "child_process";
7
7
  import { homedir } from "os";
@@ -80,6 +80,7 @@ if (cmd === "plugin-path") {
80
80
  if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd === "--no-browser" || cmd === "--server") {
81
81
  const flags = cmd?.startsWith("--") ? process.argv.slice(2) : process.argv.slice(3);
82
82
  const fullStatusline = flags.includes("--full");
83
+ let wasPluginInstalled = false;
83
84
  const { readFileSync, writeFileSync, unlinkSync } = await import("fs");
84
85
 
85
86
  function safeReadJson(filePath) {
@@ -120,6 +121,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
120
121
  // doesn't detect new versions from local sources (#37252).
121
122
  run(`claude plugin marketplace add "${pluginRoot}"`);
122
123
  const installed = run(`claude plugin list`)?.includes("patchcord");
124
+ wasPluginInstalled = !!installed;
123
125
  if (installed) {
124
126
  run(`claude plugin update patchcord@patchcord-marketplace`);
125
127
  globalChanges.push("Claude Code plugin updated");
@@ -155,12 +157,16 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
155
157
  }
156
158
  }
157
159
 
158
- const enableScript = join(pluginRoot, "scripts", "enable-statusline.sh");
159
- if (existsSync(enableScript)) {
160
- const slArg = fullStatusline ? " --full" : "";
161
- const slResult = run(`bash "${enableScript}"${slArg}`);
162
- if (slResult !== null && slResult.includes("statusline")) {
163
- globalChanges.push(`Statusline${fullStatusline ? " (full)" : ""} enabled`);
160
+ // Statusline: enabled later in the Claude Code project-setup branch (prompts user).
161
+ // If --full flag passed non-interactively, install eagerly here so non-Claude-Code
162
+ // flows (e.g. setting up Codex on a machine with Claude Code) still get the upgrade.
163
+ if (fullStatusline) {
164
+ const enableScript = join(pluginRoot, "scripts", "enable-statusline.sh");
165
+ if (existsSync(enableScript)) {
166
+ const slResult = run(`bash "${enableScript}" --full`);
167
+ if (slResult !== null && slResult.includes("statusline")) {
168
+ globalChanges.push("Statusline (full) enabled");
169
+ }
164
170
  }
165
171
  }
166
172
  }
@@ -218,10 +224,10 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
218
224
  cpSync(join(pluginRoot, "skills", "patchcord-wait", "SKILL.md"), join(geminiWaitDir, "SKILL.md"));
219
225
  geminiChanged = true;
220
226
  }
221
- if (!existsSync(join(geminiCmdDir, "patchcord.toml"))) {
227
+ if (!existsSync(join(geminiCmdDir, "inbox.toml"))) {
222
228
  mkdirSync(geminiCmdDir, { recursive: true });
223
- cpSync(join(pluginRoot, "commands", "patchcord.toml"), join(geminiCmdDir, "patchcord.toml"));
224
- cpSync(join(pluginRoot, "commands", "patchcord-wait.toml"), join(geminiCmdDir, "patchcord-wait.toml"));
229
+ cpSync(join(pluginRoot, "commands", "inbox.toml"), join(geminiCmdDir, "inbox.toml"));
230
+ cpSync(join(pluginRoot, "commands", "wait.toml"), join(geminiCmdDir, "wait.toml"));
225
231
  geminiChanged = true;
226
232
  }
227
233
  if (geminiChanged) globalChanges.push("Gemini CLI skills + commands installed");
@@ -993,14 +999,71 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
993
999
  }
994
1000
  console.log(`\n ${green}✓${r} Claude Code configured: ${dim}${mcpPath}${r}`);
995
1001
 
996
- // Enable patchcord statusline (agent identity + inbox count in status bar)
997
- try {
998
- const enableScript = join(pluginRoot, "scripts", "enable-statusline.sh");
999
- if (existsSync(enableScript)) {
1000
- execSync(`bash "${enableScript}" --full`, { stdio: "ignore" });
1001
- console.log(` ${green}✓${r} Statusline enabled`);
1002
+ // Statusline: ask whether to enable full mode (unless --full flag passed or already configured).
1003
+ const enableScript = join(pluginRoot, "scripts", "enable-statusline.sh");
1004
+ if (existsSync(enableScript)) {
1005
+ let currentStatusLine = "";
1006
+ const claudeSettingsPath = join(HOME, ".claude", "settings.json");
1007
+ if (existsSync(claudeSettingsPath)) {
1008
+ try {
1009
+ const s = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
1010
+ currentStatusLine = s?.statusLine?.command || "";
1011
+ } catch {}
1002
1012
  }
1003
- } catch {}
1013
+ const hasPatchcordStatusLine = currentStatusLine.includes("patchcord");
1014
+
1015
+ let installFull = null; // true = install, null = skip
1016
+ if (fullStatusline) {
1017
+ installFull = true; // already handled in global setup, but re-run is idempotent
1018
+ } else if (!hasPatchcordStatusLine) {
1019
+ // Build preview with user's real values
1020
+ const dirName = basename(cwd) || "project";
1021
+ let branchPart = "";
1022
+ const gitBranch = run(`git -C "${cwd}" symbolic-ref --short HEAD 2>/dev/null`);
1023
+ if (gitBranch) {
1024
+ const dirty = run(`git -C "${cwd}" status --porcelain 2>/dev/null`);
1025
+ const star = dirty ? `${red}*${green}` : "";
1026
+ branchPart = ` ${green}(${gitBranch}${star})${r}`;
1027
+ }
1028
+ const idStr = identity || "your-agent@namespace";
1029
+ const idParts = idStr.split("@");
1030
+ const idPart = idParts.length === 2
1031
+ ? `${white}${idParts[0]}${r}${dim}@${idParts[1]}${r}`
1032
+ : `${white}${idStr}${r}`;
1033
+ const machinePart = hostname ? ` ${dim}(${hostname})${r}` : "";
1034
+ const sep = `${dim} │ ${r}`;
1035
+ const blue = "\x1b[38;2;0;153;255m";
1036
+ const example = `${blue}Claude Opus 4.7${r}${sep}${green}0%${r}${sep}${cyan}${dirName}${r}${branchPart}${sep}${idPart}${machinePart}`;
1037
+
1038
+ console.log(``);
1039
+ console.log(` Patchcord can show live agent identity + context in Claude Code's status bar.`);
1040
+ console.log(``);
1041
+ console.log(` Full mode: ${example}`);
1042
+ console.log(``);
1043
+
1044
+ const defaultYes = !wasPluginInstalled;
1045
+ const promptStr = defaultYes
1046
+ ? ` ${bold}Install full status bar?${r} ${dim}(Y/n):${r} `
1047
+ : ` ${bold}Install full status bar?${r} ${dim}(y/N):${r} `;
1048
+ const { createInterface: createRLS } = await import("readline");
1049
+ const rlS = createRLS({ input: process.stdin, output: process.stdout });
1050
+ const answer = await new Promise((resolve) => rlS.question(promptStr, resolve));
1051
+ rlS.close();
1052
+ const a = (answer || "").trim().toLowerCase();
1053
+ const yes = defaultYes
1054
+ ? !(a === "n" || a === "no")
1055
+ : (a === "y" || a === "yes");
1056
+ if (yes) installFull = true;
1057
+ }
1058
+ // else: existing patchcord statusline — leave it alone
1059
+
1060
+ if (installFull === true) {
1061
+ try {
1062
+ execSync(`bash "${enableScript}" --full`, { stdio: "ignore" });
1063
+ console.log(` ${green}✓${r} Statusline enabled`);
1064
+ } catch {}
1065
+ }
1066
+ }
1004
1067
  }
1005
1068
 
1006
1069
  // Warn about gitignore for per-project configs with tokens
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.98",
3
+ "version": "0.4.0",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -122,7 +122,9 @@ Send the returned `path` to the other agent in your message so they can download
122
122
  ## Rules
123
123
 
124
124
  - Do the work first, reply second. Never reply before completing the task.
125
- - Do not send ack-only replies to "ok", "noted", "seen", "thanks", or conversation-ending signals. Just read them and move on.
125
+ - **Do not reply to acks.** "ok", "noted", "seen", "thanks", "good progress", "keep running" read them and move on. If you must close the thread: `reply(id, resolve=true)` with NO content.
126
+ - **resolve=true with ack-only content is an anti-pattern.** `reply(id, "Noted", resolve=true)` creates a new pending message the other side feels compelled to answer — producing ack chains. Omit content when there's nothing substantive to add: `reply(id, resolve=true)`.
127
+ - **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.
126
128
  - Do not show raw JSON to the user unless they explicitly ask for it.
127
129
  - Use `agent@namespace` when the online list shows multiple namespaces for the same agent name.
128
130
  - Keep Patchcord config project-local. Do not rely on global shell exports.
@@ -53,7 +53,7 @@ Use the dominant topic of your current conversation as the tag. Keep it short (1
53
53
 
54
54
  6. **Never show raw JSON** - summarize naturally.
55
55
 
56
- 7. **Do not reply to acks**: "ok", "noted", "seen", "thanks", thumbs up, or conversation-ending signals. Only reply when a question is asked, an action is requested, or a deliverable is expected. Use `resolve=true` on your reply when a thread is done. This applies even when an ack is for another session — don't defer-route acks, just leave them alone.
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
57
 
58
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
59
 
@@ -121,6 +121,8 @@ Named threads group related messages between a pair of agents. Use them for mult
121
121
  - Never ask "want me to wait?" - check presence and wait or don't based on that.
122
122
  - Never show raw JSON to the human - summarize naturally.
123
123
  - Cross-namespace agents: use `agent@namespace` syntax in send_message when targeting a specific namespace.
124
- - Do not reply to messages that don't need a response: acks, "ok", "noted", "seen", thumbs up, confirmations, "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.
124
+ - **Do not reply to acks.** "ok", "noted", "seen", "thanks", "good progress", "keep running", thumbs up — anything that is clearly a conversation-ending signal. Just read them and move on. If you must close the thread, use `reply(id, resolve=true)` with NO content. Never send a text reply to an ack.
125
+ - **resolve=true with ack-only content is an anti-pattern.** `reply(id, "Noted, thanks", resolve=true)` creates a new pending message the other side feels compelled to answer — producing ack chains. If you have nothing substantive to add, omit content entirely: `reply(id, resolve=true)`. Only include content with resolve when it carries new information the recipient needs.
126
+ - **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.
125
127
  - MCP tools are cached at session start. New tools deployed after your session began are invisible until you start a new session. If a tool you expect is missing, this is why.
126
128
  - Agent names change frequently. Do not memorize or hardcode them. Check inbox() for recent activity. When unsure which agent to message, ask the human.
@@ -22,3 +22,5 @@ Loop until timeout or the human interrupts.
22
22
  If `wait_for_message()` errors, fall back to polling `inbox()` every 10-15 seconds instead of stopping the loop.
23
23
 
24
24
  Do not ask the human for permission to reply - just do the work, reply with results, then report.
25
+
26
+ **No ack chains.** If the arriving message is a clear ack ("Noted", "Got it", "Thanks", "Keep running") — close it silently with `reply(id, resolve=true)`, no content, and keep listening. Never text-reply to an ack. Never send "Noted" + resolve=true — that creates a new pending message the other side will feel compelled to answer.