patchcord 0.5.14 → 0.5.16
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 +174 -176
- package/package.json +1 -1
- package/scripts/subscribe.mjs +28 -0
- package/skills/inbox/SKILL.md +17 -6
package/bin/patchcord.mjs
CHANGED
|
@@ -97,6 +97,43 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
97
97
|
} catch { return null; }
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Read+merge+write a JSON config file. If the file exists but its contents
|
|
101
|
+
// can't be parsed, REFUSE to write — silently overwriting would erase
|
|
102
|
+
// unrelated MCP servers, settings, or hand-edits the user has in there.
|
|
103
|
+
// Returns true on success, false when skipped due to a parse failure (so
|
|
104
|
+
// the caller can suppress its "configured" message).
|
|
105
|
+
function updateJsonConfig(filePath, mutate) {
|
|
106
|
+
let parsed = {};
|
|
107
|
+
if (existsSync(filePath)) {
|
|
108
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
109
|
+
try {
|
|
110
|
+
// JSONC-tolerant: Zed/Gemini settings allow //, /* */, trailing commas.
|
|
111
|
+
const cleaned = raw
|
|
112
|
+
.replace(/\/\/.*$/gm, "")
|
|
113
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
114
|
+
.replace(/,\s*([}\]])/g, "$1");
|
|
115
|
+
const result = cleaned.trim() ? JSON.parse(cleaned) : {};
|
|
116
|
+
// null / arrays / primitives aren't config objects we can merge into;
|
|
117
|
+
// refuse rather than blow them away.
|
|
118
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
119
|
+
console.log(`\n ${yellow}⚠${r} Skipped ${filePath} — existing file is not a JSON object.`);
|
|
120
|
+
console.log(` Replace it with a valid object ({...}) or remove it, then re-run.`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
parsed = result;
|
|
124
|
+
} catch {
|
|
125
|
+
console.log(`\n ${yellow}⚠${r} Skipped ${filePath} — could not parse existing JSON.`);
|
|
126
|
+
console.log(` Fix the file by hand and re-run the installer. We will not`);
|
|
127
|
+
console.log(` overwrite it: that would erase your other servers/settings.`);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
mutate(parsed);
|
|
132
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
133
|
+
writeFileSync(filePath, JSON.stringify(parsed, null, 2) + "\n");
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
console.log(`
|
|
101
138
|
___ ____ ___ ____ _ _ ____ ____ ____ ___
|
|
102
139
|
|__] |__| | | |__| | | | |__/ | \\
|
|
@@ -661,20 +698,14 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
661
698
|
},
|
|
662
699
|
};
|
|
663
700
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
writeFileSync(cursorPath, JSON.stringify(cursorConfig, null, 2) + "\n");
|
|
672
|
-
}
|
|
673
|
-
} else {
|
|
674
|
-
writeFileSync(cursorPath, JSON.stringify(cursorConfig, null, 2) + "\n");
|
|
701
|
+
const cursorOk = updateJsonConfig(cursorPath, (obj) => {
|
|
702
|
+
obj.mcpServers = obj.mcpServers || {};
|
|
703
|
+
obj.mcpServers.patchcord = cursorConfig.mcpServers.patchcord;
|
|
704
|
+
});
|
|
705
|
+
if (cursorOk) {
|
|
706
|
+
console.log(`\n ${green}✓${r} Cursor configured: ${dim}${cursorPath}${r}`);
|
|
707
|
+
console.log(` ${dim}Per-project only — other projects won't see this agent.${r}`);
|
|
675
708
|
}
|
|
676
|
-
console.log(`\n ${green}✓${r} Cursor configured: ${dim}${cursorPath}${r}`);
|
|
677
|
-
console.log(` ${dim}Per-project only — other projects won't see this agent.${r}`);
|
|
678
709
|
} else if (isWindsurf) {
|
|
679
710
|
// Windsurf: global only (~/.codeium/windsurf/mcp_config.json)
|
|
680
711
|
const wsPath = join(HOME, ".codeium", "windsurf", "mcp_config.json");
|
|
@@ -690,84 +721,72 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
690
721
|
},
|
|
691
722
|
};
|
|
692
723
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
} catch {
|
|
701
|
-
writeFileSync(wsPath, JSON.stringify(wsConfig, null, 2) + "\n");
|
|
702
|
-
}
|
|
703
|
-
} else {
|
|
704
|
-
mkdirSync(join(HOME, ".codeium", "windsurf"), { recursive: true });
|
|
705
|
-
writeFileSync(wsPath, JSON.stringify(wsConfig, null, 2) + "\n");
|
|
724
|
+
const wsOk = updateJsonConfig(wsPath, (obj) => {
|
|
725
|
+
obj.mcpServers = obj.mcpServers || {};
|
|
726
|
+
obj.mcpServers.patchcord = wsConfig.mcpServers.patchcord;
|
|
727
|
+
});
|
|
728
|
+
if (wsOk) {
|
|
729
|
+
console.log(`\n ${green}✓${r} Windsurf configured: ${dim}${wsPath}${r}`);
|
|
730
|
+
console.log(` ${yellow}Global config — all Windsurf projects share this agent.${r}`);
|
|
706
731
|
}
|
|
707
|
-
console.log(`\n ${green}✓${r} Windsurf configured: ${dim}${wsPath}${r}`);
|
|
708
|
-
console.log(` ${yellow}Global config — all Windsurf projects share this agent.${r}`);
|
|
709
732
|
} else if (isGemini) {
|
|
710
733
|
// Gemini CLI: global only (~/.gemini/settings.json)
|
|
711
734
|
const geminiPath = join(HOME, ".gemini", "settings.json");
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
735
|
+
const geminiOk = updateJsonConfig(geminiPath, (obj) => {
|
|
736
|
+
obj.mcpServers = obj.mcpServers || {};
|
|
737
|
+
obj.mcpServers.patchcord = {
|
|
738
|
+
httpUrl: `${serverUrl}/mcp`,
|
|
739
|
+
headers: {
|
|
740
|
+
Authorization: `Bearer ${token}`,
|
|
741
|
+
"X-Patchcord-Machine": hostname,
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
// Clean up deprecated tools.allowed if present (removed in Gemini CLI 1.0)
|
|
745
|
+
if (obj.tools?.allowed) {
|
|
746
|
+
obj.tools.allowed = obj.tools.allowed.filter(t => !t.startsWith("mcp_patchcord_"));
|
|
747
|
+
if (obj.tools.allowed.length === 0) delete obj.tools;
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
if (geminiOk) {
|
|
751
|
+
console.log(`\n ${green}✓${r} Gemini CLI configured: ${dim}${geminiPath}${r}`);
|
|
752
|
+
console.log(` ${yellow}Global config — all Gemini CLI projects share this agent.${r}`);
|
|
725
753
|
}
|
|
726
|
-
mkdirSync(join(HOME, ".gemini"), { recursive: true });
|
|
727
|
-
writeFileSync(geminiPath, JSON.stringify(geminiSettings, null, 2) + "\n");
|
|
728
|
-
console.log(`\n ${green}✓${r} Gemini CLI configured: ${dim}${geminiPath}${r}`);
|
|
729
|
-
console.log(` ${yellow}Global config — all Gemini CLI projects share this agent.${r}`);
|
|
730
754
|
} else if (isZed) {
|
|
731
755
|
// Zed: global settings.json → context_servers
|
|
732
756
|
const zedPath = process.platform === "darwin"
|
|
733
757
|
? join(HOME, "Library", "Application Support", "Zed", "settings.json")
|
|
734
758
|
: join(HOME, ".config", "zed", "settings.json");
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
console.log(`\n ${green}✓${r} Zed configured: ${dim}${zedPath}${r}`);
|
|
750
|
-
console.log(` ${yellow}Global config — all Zed projects share this agent.${r}`);
|
|
759
|
+
const zedOk = updateJsonConfig(zedPath, (obj) => {
|
|
760
|
+
obj.context_servers = obj.context_servers || {};
|
|
761
|
+
obj.context_servers.patchcord = {
|
|
762
|
+
url: `${serverUrl}/mcp`,
|
|
763
|
+
headers: {
|
|
764
|
+
Authorization: `Bearer ${token}`,
|
|
765
|
+
"X-Patchcord-Machine": hostname,
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
});
|
|
769
|
+
if (zedOk) {
|
|
770
|
+
console.log(`\n ${green}✓${r} Zed configured: ${dim}${zedPath}${r}`);
|
|
771
|
+
console.log(` ${yellow}Global config — all Zed projects share this agent.${r}`);
|
|
772
|
+
}
|
|
751
773
|
} else if (isOpenCode) {
|
|
752
774
|
// OpenCode: per-project opencode.json → mcp
|
|
753
775
|
const ocPath = join(cwd, "opencode.json");
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
776
|
+
const ocOk = updateJsonConfig(ocPath, (obj) => {
|
|
777
|
+
obj.mcp = obj.mcp || {};
|
|
778
|
+
obj.mcp.patchcord = {
|
|
779
|
+
type: "remote",
|
|
780
|
+
url: `${serverUrl}/mcp`,
|
|
781
|
+
headers: {
|
|
782
|
+
Authorization: `Bearer ${token}`,
|
|
783
|
+
"X-Patchcord-Machine": hostname,
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
if (ocOk) {
|
|
788
|
+
console.log(`\n ${green}✓${r} OpenCode configured: ${dim}${ocPath}${r}`);
|
|
759
789
|
}
|
|
760
|
-
if (!ocConfig.mcp) ocConfig.mcp = {};
|
|
761
|
-
ocConfig.mcp.patchcord = {
|
|
762
|
-
type: "remote",
|
|
763
|
-
url: `${serverUrl}/mcp`,
|
|
764
|
-
headers: {
|
|
765
|
-
Authorization: `Bearer ${token}`,
|
|
766
|
-
"X-Patchcord-Machine": hostname,
|
|
767
|
-
},
|
|
768
|
-
};
|
|
769
|
-
writeFileSync(ocPath, JSON.stringify(ocConfig, null, 2) + "\n");
|
|
770
|
-
console.log(`\n ${green}✓${r} OpenCode configured: ${dim}${ocPath}${r}`);
|
|
771
790
|
} else if (isOpenClaw) {
|
|
772
791
|
// OpenClaw: global ~/.openclaw/openclaw.json → mcp.servers
|
|
773
792
|
// Try CLI first, fall back to direct file write
|
|
@@ -785,28 +804,23 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
785
804
|
console.log(`\n ${green}✓${r} OpenClaw configured via CLI: ${dim}openclaw mcp set${r}`);
|
|
786
805
|
} else {
|
|
787
806
|
// CLI not available — write config directly
|
|
788
|
-
const
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
807
|
+
const openclawPath = join(HOME, ".openclaw", "openclaw.json");
|
|
808
|
+
const openclawOk = updateJsonConfig(openclawPath, (obj) => {
|
|
809
|
+
obj.mcp = obj.mcp || {};
|
|
810
|
+
obj.mcp.servers = obj.mcp.servers || {};
|
|
811
|
+
obj.mcp.servers.patchcord = {
|
|
812
|
+
url: `${serverUrl}/mcp`,
|
|
813
|
+
transport: "streamable-http",
|
|
814
|
+
headers: {
|
|
815
|
+
Authorization: `Bearer ${token}`,
|
|
816
|
+
"X-Patchcord-Machine": hostname,
|
|
817
|
+
},
|
|
818
|
+
connectionTimeoutMs: 300000,
|
|
819
|
+
};
|
|
820
|
+
});
|
|
821
|
+
if (openclawOk) {
|
|
822
|
+
console.log(`\n ${green}✓${r} OpenClaw configured: ${dim}${openclawPath}${r}`);
|
|
795
823
|
}
|
|
796
|
-
if (!openclawConfig.mcp) openclawConfig.mcp = {};
|
|
797
|
-
if (!openclawConfig.mcp.servers) openclawConfig.mcp.servers = {};
|
|
798
|
-
openclawConfig.mcp.servers.patchcord = {
|
|
799
|
-
url: `${serverUrl}/mcp`,
|
|
800
|
-
transport: "streamable-http",
|
|
801
|
-
headers: {
|
|
802
|
-
Authorization: `Bearer ${token}`,
|
|
803
|
-
"X-Patchcord-Machine": hostname,
|
|
804
|
-
},
|
|
805
|
-
connectionTimeoutMs: 300000,
|
|
806
|
-
};
|
|
807
|
-
mkdirSync(openclawDir, { recursive: true });
|
|
808
|
-
writeFileSync(openclawPath, JSON.stringify(openclawConfig, null, 2) + "\n");
|
|
809
|
-
console.log(`\n ${green}✓${r} OpenClaw configured: ${dim}${openclawPath}${r}`);
|
|
810
824
|
}
|
|
811
825
|
console.log(` ${yellow}Global config — all OpenClaw channels share this agent.${r}`);
|
|
812
826
|
console.log(` ${dim}Run: openclaw gateway restart${r}`);
|
|
@@ -818,32 +832,28 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
818
832
|
// Antigravity: global ~/.gemini/antigravity/mcp_config.json → mcpServers
|
|
819
833
|
const agDir = join(HOME, ".gemini", "antigravity");
|
|
820
834
|
const agPath = join(agDir, "mcp_config.json");
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
835
|
+
const agOk = updateJsonConfig(agPath, (obj) => {
|
|
836
|
+
obj.mcpServers = obj.mcpServers || {};
|
|
837
|
+
obj.mcpServers.patchcord = {
|
|
838
|
+
serverUrl: `${serverUrl}/mcp`,
|
|
839
|
+
headers: {
|
|
840
|
+
Authorization: `Bearer ${token}`,
|
|
841
|
+
"X-Patchcord-Machine": hostname,
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
});
|
|
845
|
+
if (agOk) {
|
|
846
|
+
console.log(`\n ${green}✓${r} Antigravity configured: ${dim}${agPath}${r}`);
|
|
847
|
+
// Install global skills
|
|
848
|
+
const agSkillDir = join(agDir, "skills", "patchcord");
|
|
849
|
+
const agWaitDir = join(agDir, "skills", "patchcord-wait");
|
|
850
|
+
mkdirSync(agSkillDir, { recursive: true });
|
|
851
|
+
mkdirSync(agWaitDir, { recursive: true });
|
|
852
|
+
cpSync(join(pluginRoot, "skills", "inbox", "SKILL.md"), join(agSkillDir, "SKILL.md"));
|
|
853
|
+
cpSync(join(pluginRoot, "skills", "wait", "SKILL.md"), join(agWaitDir, "SKILL.md"));
|
|
854
|
+
console.log(` ${green}✓${r} Skills installed: ${dim}patchcord${r}, ${dim}patchcord-wait${r}`);
|
|
855
|
+
console.log(` ${yellow}Global config — all Antigravity projects share this agent.${r}`);
|
|
826
856
|
}
|
|
827
|
-
if (!agConfig.mcpServers) agConfig.mcpServers = {};
|
|
828
|
-
agConfig.mcpServers.patchcord = {
|
|
829
|
-
serverUrl: `${serverUrl}/mcp`,
|
|
830
|
-
headers: {
|
|
831
|
-
Authorization: `Bearer ${token}`,
|
|
832
|
-
"X-Patchcord-Machine": hostname,
|
|
833
|
-
},
|
|
834
|
-
};
|
|
835
|
-
mkdirSync(agDir, { recursive: true });
|
|
836
|
-
writeFileSync(agPath, JSON.stringify(agConfig, null, 2) + "\n");
|
|
837
|
-
console.log(`\n ${green}✓${r} Antigravity configured: ${dim}${agPath}${r}`);
|
|
838
|
-
// Install global skills
|
|
839
|
-
const agSkillDir = join(agDir, "skills", "patchcord");
|
|
840
|
-
const agWaitDir = join(agDir, "skills", "patchcord-wait");
|
|
841
|
-
mkdirSync(agSkillDir, { recursive: true });
|
|
842
|
-
mkdirSync(agWaitDir, { recursive: true });
|
|
843
|
-
cpSync(join(pluginRoot, "skills", "inbox", "SKILL.md"), join(agSkillDir, "SKILL.md"));
|
|
844
|
-
cpSync(join(pluginRoot, "skills", "wait", "SKILL.md"), join(agWaitDir, "SKILL.md"));
|
|
845
|
-
console.log(` ${green}✓${r} Skills installed: ${dim}patchcord${r}, ${dim}patchcord-wait${r}`);
|
|
846
|
-
console.log(` ${yellow}Global config — all Antigravity projects share this agent.${r}`);
|
|
847
857
|
} else if (isCline) {
|
|
848
858
|
// Cline VS Code extension: global cline_mcp_settings.json
|
|
849
859
|
// Config lives in VS Code's globalStorage for saoudrizwan.claude-dev
|
|
@@ -884,24 +894,22 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
884
894
|
}
|
|
885
895
|
|
|
886
896
|
const clinePath = join(clineSettingsDir, "cline_mcp_settings.json");
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
897
|
+
const clineOk = updateJsonConfig(clinePath, (obj) => {
|
|
898
|
+
obj.mcpServers = obj.mcpServers || {};
|
|
899
|
+
obj.mcpServers.patchcord = {
|
|
900
|
+
url: `${serverUrl}/mcp`,
|
|
901
|
+
headers: {
|
|
902
|
+
Authorization: `Bearer ${token}`,
|
|
903
|
+
"X-Patchcord-Machine": hostname,
|
|
904
|
+
},
|
|
905
|
+
disabled: false,
|
|
906
|
+
alwaysAllow: [],
|
|
907
|
+
};
|
|
908
|
+
});
|
|
909
|
+
if (clineOk) {
|
|
910
|
+
console.log(`\n ${green}✓${r} Cline configured: ${dim}${clinePath}${r}`);
|
|
911
|
+
console.log(` ${yellow}Global config — all Cline projects share this agent.${r}`);
|
|
890
912
|
}
|
|
891
|
-
if (!clineConfig.mcpServers) clineConfig.mcpServers = {};
|
|
892
|
-
clineConfig.mcpServers.patchcord = {
|
|
893
|
-
url: `${serverUrl}/mcp`,
|
|
894
|
-
headers: {
|
|
895
|
-
Authorization: `Bearer ${token}`,
|
|
896
|
-
"X-Patchcord-Machine": hostname,
|
|
897
|
-
},
|
|
898
|
-
disabled: false,
|
|
899
|
-
alwaysAllow: [],
|
|
900
|
-
};
|
|
901
|
-
mkdirSync(clineSettingsDir, { recursive: true });
|
|
902
|
-
writeFileSync(clinePath, JSON.stringify(clineConfig, null, 2) + "\n");
|
|
903
|
-
console.log(`\n ${green}✓${r} Cline configured: ${dim}${clinePath}${r}`);
|
|
904
|
-
console.log(` ${yellow}Global config — all Cline projects share this agent.${r}`);
|
|
905
913
|
} else if (isVSCode) {
|
|
906
914
|
// VS Code: write .vscode/mcp.json (per-project)
|
|
907
915
|
const vscodeDir = join(cwd, ".vscode");
|
|
@@ -920,20 +928,14 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
920
928
|
},
|
|
921
929
|
};
|
|
922
930
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
writeFileSync(vscodePath, JSON.stringify(vscodeConfig, null, 2) + "\n");
|
|
931
|
-
}
|
|
932
|
-
} else {
|
|
933
|
-
writeFileSync(vscodePath, JSON.stringify(vscodeConfig, null, 2) + "\n");
|
|
931
|
+
const vscodeOk = updateJsonConfig(vscodePath, (obj) => {
|
|
932
|
+
obj.servers = obj.servers || {};
|
|
933
|
+
obj.servers.patchcord = vscodeConfig.servers.patchcord;
|
|
934
|
+
});
|
|
935
|
+
if (vscodeOk) {
|
|
936
|
+
console.log(`\n ${green}✓${r} VS Code configured: ${dim}${vscodePath}${r}`);
|
|
937
|
+
console.log(` ${dim}Requires GitHub Copilot extension with agent mode enabled.${r}`);
|
|
934
938
|
}
|
|
935
|
-
console.log(`\n ${green}✓${r} VS Code configured: ${dim}${vscodePath}${r}`);
|
|
936
|
-
console.log(` ${dim}Requires GitHub Copilot extension with agent mode enabled.${r}`);
|
|
937
939
|
} else if (isCodex) {
|
|
938
940
|
// Codex: write MCP config + per-project skills + global plugin
|
|
939
941
|
// Per-project skills (working @patchcord in Codex v0.117)
|
|
@@ -1010,19 +1012,17 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1010
1012
|
|
|
1011
1013
|
// Personal marketplace entry (relative path from marketplace root)
|
|
1012
1014
|
const marketplacePath = join(marketplaceDir, "marketplace.json");
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
category: "Productivity",
|
|
1015
|
+
updateJsonConfig(marketplacePath, (obj) => {
|
|
1016
|
+
if (!obj.name) obj.name = "patchcord";
|
|
1017
|
+
if (!obj.interface) obj.interface = { displayName: "Patchcord" };
|
|
1018
|
+
obj.plugins = (obj.plugins || []).filter(p => p.name !== "patchcord");
|
|
1019
|
+
obj.plugins.push({
|
|
1020
|
+
name: "patchcord",
|
|
1021
|
+
source: { source: "local", path: "./patchcord" },
|
|
1022
|
+
policy: { installation: "INSTALLED_BY_DEFAULT", authentication: "ON_INSTALL" },
|
|
1023
|
+
category: "Productivity",
|
|
1024
|
+
});
|
|
1024
1025
|
});
|
|
1025
|
-
writeFileSync(marketplacePath, JSON.stringify(marketplace, null, 2) + "\n");
|
|
1026
1026
|
|
|
1027
1027
|
// Also install global skills (working @patchcord — plugin/read broken in Codex v0.117)
|
|
1028
1028
|
const globalSkillDir = join(homedir(), ".agents", "skills", "patchcord");
|
|
@@ -1052,17 +1052,15 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1052
1052
|
},
|
|
1053
1053
|
};
|
|
1054
1054
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
} else {
|
|
1065
|
-
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
1055
|
+
const mcpOk = updateJsonConfig(mcpPath, (obj) => {
|
|
1056
|
+
obj.mcpServers = obj.mcpServers || {};
|
|
1057
|
+
obj.mcpServers.patchcord = mcpConfig.mcpServers.patchcord;
|
|
1058
|
+
});
|
|
1059
|
+
if (!mcpOk) {
|
|
1060
|
+
// Existing .mcp.json was malformed — helper already explained.
|
|
1061
|
+
// Bail with non-zero so wrapper scripts notice; the user fixes
|
|
1062
|
+
// their JSON and re-runs.
|
|
1063
|
+
process.exit(1);
|
|
1066
1064
|
}
|
|
1067
1065
|
console.log(`\n ${green}✓${r} Claude Code configured: ${dim}${mcpPath}${r}`);
|
|
1068
1066
|
|
package/package.json
CHANGED
package/scripts/subscribe.mjs
CHANGED
|
@@ -16,6 +16,12 @@ import { connect as wsConnect } from "./lib/ws.mjs";
|
|
|
16
16
|
const JWT_REFRESH_SAFETY_MARGIN_SEC = 120;
|
|
17
17
|
const HEARTBEAT_INTERVAL_MS = 25_000;
|
|
18
18
|
const RECONNECT_BACKOFF_MS = [1000, 2000, 4000, 8000, 15_000, 30_000];
|
|
19
|
+
// Freshness watchdog: if we haven't received ANY frame from the server
|
|
20
|
+
// (heartbeat replies, postgres_changes, system events) for this long, the
|
|
21
|
+
// WS is silently dead — phx replies should arrive every ~25s, so a 90s
|
|
22
|
+
// gap means three missed heartbeats. Force a reconnect via the outer loop.
|
|
23
|
+
const FRESHNESS_CHECK_INTERVAL_MS = 30_000;
|
|
24
|
+
const FRESHNESS_STALE_MS = 90_000;
|
|
19
25
|
|
|
20
26
|
// Guarantee a terminal stderr line on any unhandled failure so the agent
|
|
21
27
|
// reading Monitor's output file always sees WHY the process died.
|
|
@@ -230,6 +236,8 @@ function runOnce(ticket, baseUrl, token, refreshTicket) {
|
|
|
230
236
|
let ref = 1;
|
|
231
237
|
let heartbeatTimer = null;
|
|
232
238
|
let refreshTimer = null;
|
|
239
|
+
let freshnessTimer = null;
|
|
240
|
+
let lastEventAt = Date.now();
|
|
233
241
|
let currentJwt = ticket.jwt;
|
|
234
242
|
let settled = false;
|
|
235
243
|
|
|
@@ -238,6 +246,7 @@ function runOnce(ticket, baseUrl, token, refreshTicket) {
|
|
|
238
246
|
settled = true;
|
|
239
247
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
240
248
|
if (refreshTimer) clearTimeout(refreshTimer);
|
|
249
|
+
if (freshnessTimer) clearInterval(freshnessTimer);
|
|
241
250
|
try {
|
|
242
251
|
ws.close();
|
|
243
252
|
} catch (_) {}
|
|
@@ -283,6 +292,21 @@ function runOnce(ticket, baseUrl, token, refreshTicket) {
|
|
|
283
292
|
} catch (_) {}
|
|
284
293
|
}, HEARTBEAT_INTERVAL_MS);
|
|
285
294
|
|
|
295
|
+
// Freshness watchdog. Phoenix replies to every heartbeat with a
|
|
296
|
+
// phx_reply frame; if those stop arriving (silent network drop,
|
|
297
|
+
// backgrounded socket after laptop sleep, server-side eviction
|
|
298
|
+
// without close frame), the WS appears open but is dead. Force a
|
|
299
|
+
// reconnect via the outer loop — done(err) increments backoffIdx.
|
|
300
|
+
freshnessTimer = setInterval(() => {
|
|
301
|
+
const stale = Date.now() - lastEventAt;
|
|
302
|
+
if (stale > FRESHNESS_STALE_MS) {
|
|
303
|
+
process.stderr.write(
|
|
304
|
+
`subscribe: ws stale ${Math.round(stale / 1000)}s, forcing reconnect\n`
|
|
305
|
+
);
|
|
306
|
+
done(new Error(`ws stale ${Math.round(stale / 1000)}s`));
|
|
307
|
+
}
|
|
308
|
+
}, FRESHNESS_CHECK_INTERVAL_MS);
|
|
309
|
+
|
|
286
310
|
const scheduleRefresh = (ttlSec) => {
|
|
287
311
|
const refreshIn = Math.max((ttlSec - JWT_REFRESH_SAFETY_MARGIN_SEC) * 1000, 30_000);
|
|
288
312
|
refreshTimer = setTimeout(doRefresh, refreshIn);
|
|
@@ -331,6 +355,10 @@ function runOnce(ticket, baseUrl, token, refreshTicket) {
|
|
|
331
355
|
});
|
|
332
356
|
|
|
333
357
|
ws.on("message", (raw) => {
|
|
358
|
+
// Any inbound frame counts as proof of life — phx_reply, system,
|
|
359
|
+
// presence_state, postgres_changes, etc. Reset BEFORE the parse
|
|
360
|
+
// attempt so even malformed frames count.
|
|
361
|
+
lastEventAt = Date.now();
|
|
334
362
|
let frame;
|
|
335
363
|
try {
|
|
336
364
|
frame = JSON.parse(raw);
|
package/skills/inbox/SKILL.md
CHANGED
|
@@ -32,19 +32,30 @@ The user can undo any change in 3 seconds with git. A wrong action costs nothing
|
|
|
32
32
|
|
|
33
33
|
## On session start or when prompted by a hook
|
|
34
34
|
|
|
35
|
-
Call inbox(). It returns pending messages
|
|
35
|
+
Call inbox(). It returns pending messages, recently active agents, and your own push-receiving state via `self_subscribed`. Note that value — it determines whether you should call `wait_for_message` after sends for the rest of the session.
|
|
36
|
+
|
|
37
|
+
If `subscribe_appears_down: true` is in the response, your subscribe.mjs was running but appears dead. Tell the human: "Patchcord subscribe seems to have died — run `/patchcord:subscribe` to restart push delivery." Do not try to restart it yourself.
|
|
36
38
|
|
|
37
39
|
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 do the work described in each message, then reply with what you did, then tell the human what you received and what you did about it.
|
|
38
40
|
|
|
39
41
|
## Sending
|
|
40
42
|
|
|
41
|
-
1. inbox() - clear any pending messages that block outbound sends.
|
|
43
|
+
1. inbox() - clear any pending messages that block outbound sends. From the response, note `self_subscribed` (your own push-receiving state).
|
|
42
44
|
2. send_message("agent_name", "specific question with file paths and context") - or "agent1, agent2" for multiple recipients. Use `@username` for cross-user Gate messaging. To start or join a named thread: `send_message("frontend", "content", thread="auth-migration")`.
|
|
43
|
-
3.
|
|
45
|
+
3. Decide whether to wait based on **two signals** in the send response:
|
|
46
|
+
- `self_subscribed` (from the most recent inbox call) — are YOU push-receiving?
|
|
47
|
+
- `recipient_subscribed` (in the send response) — is the recipient push-receiving?
|
|
48
|
+
|
|
49
|
+
| self_subscribed | recipient_subscribed | What to do |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| true | true | **Do NOT call wait_for_message.** Continue working. Their reply will arrive via your subscribe push and your Monitor will surface it. Tell the human: "Sent — [agent] will see it within seconds." |
|
|
52
|
+
| true | false | **Do NOT call wait_for_message.** Continue working. Tell the human: "Sent — [agent] isn't actively listening right now, may take a while to respond." |
|
|
53
|
+
| false | true | **Call wait_for_message** with default timeout. Recipient is live, expect a reply soon. |
|
|
54
|
+
| false | false | **Skip wait_for_message.** Tell the human: "Sent — [agent] isn't currently active. Ask them to check inbox in their session." |
|
|
44
55
|
|
|
45
|
-
Always send regardless of
|
|
56
|
+
Always send regardless of recipient state. Messages are stored and delivered when the recipient checks inbox.
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
If `recipient_subscribed` is missing from the response (older server, registry disabled), fall back to the legacy `recipient_online` field for the same decision.
|
|
48
59
|
|
|
49
60
|
If send_message fails with a send gate error: call inbox(), reply to or resolve all pending messages, then retry the send.
|
|
50
61
|
|
|
@@ -57,7 +68,7 @@ If send_message fails with a send gate error: call inbox(), reply to or resolve
|
|
|
57
68
|
- `reply(message_id, "done: [details]", resolve=true)` — work done, thread closed. Stamps `thread_resolved_at` and notifies sender.
|
|
58
69
|
- `reply(message_id, resolve=true)` — silently close a thread without sending anything (e.g. clearing misfired messages)
|
|
59
70
|
- `reply(message_id, "ack, prioritizing [other task] first", defer=true)` — you acknowledged but haven't done the work yet. The message stays in your inbox as a reminder.
|
|
60
|
-
4.
|
|
71
|
+
4. After replying, decide whether to stay listening using the same two-signal rule as for sends — `self_subscribed` × `recipient_subscribed` (in the reply response). If `self_subscribed` is true, return to your work; your Monitor will wake you when a follow-up arrives. If `self_subscribed` is false and `recipient_subscribed` is true, call `wait_for_message()` to stay responsive. Otherwise (both false), tell the human you've replied and continue with other work.
|
|
61
72
|
5. If you can't do the work, say specifically what's blocking you. Don't guess about another agent's code.
|
|
62
73
|
|
|
63
74
|
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.
|