@manuelfedele/postino 0.2.0 → 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.
package/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { loadConfig } from "./types.js";
5
- import { connect, disconnect, registerAgent, deregisterAgent } from "./valkey.js";
5
+ import { connect, disconnect, valkey, valkeySub, keys, registerAgent, deregisterAgent } from "./valkey.js";
6
6
  import { registerMessagingTools } from "./tools/messaging.js";
7
- import { startWebServer } from "./web/server.js";
7
+ import { startWebServer, getGuiState, restartOnPort } from "./web/server.js";
8
8
  const config = loadConfig();
9
9
  const server = new McpServer({
10
10
  name: "postino",
@@ -15,6 +15,35 @@ const server = new McpServer({
15
15
  },
16
16
  });
17
17
  registerMessagingTools(server, config.agentName);
18
+ function subscribeGuiTakeover() {
19
+ const channel = keys.guiTakeoverChannel();
20
+ valkeySub.subscribe(channel).catch(() => {
21
+ process.stderr.write("postino: failed to subscribe to GUI takeover channel\n");
22
+ });
23
+ valkeySub.on("message", (ch, message) => {
24
+ if (ch !== channel)
25
+ return;
26
+ let data;
27
+ try {
28
+ data = JSON.parse(message);
29
+ }
30
+ catch {
31
+ return;
32
+ }
33
+ const state = getGuiState();
34
+ // Skip if we already have a GUI on an equal or lower port
35
+ if (state.running && state.port !== null && state.port <= data.port)
36
+ return;
37
+ // Random jitter (100-500ms) so not all instances race at once
38
+ const jitter = 100 + Math.random() * 400;
39
+ setTimeout(async () => {
40
+ const ok = await restartOnPort(data.port);
41
+ if (ok) {
42
+ process.stderr.write(`postino: took over GUI on port ${data.port}\n`);
43
+ }
44
+ }, jitter);
45
+ });
46
+ }
18
47
  async function main() {
19
48
  try {
20
49
  await connect();
@@ -36,9 +65,20 @@ async function main() {
36
65
  await server.connect(transport);
37
66
  if (config.webEnabled) {
38
67
  startWebServer(config.webPort);
68
+ subscribeGuiTakeover();
39
69
  }
40
70
  process.stderr.write(`postino agent: ${config.agentName}\n`);
41
71
  const shutdown = async () => {
72
+ // If this instance had a GUI, notify others so they can take over the port
73
+ const state = getGuiState();
74
+ if (state.running && state.port !== null) {
75
+ try {
76
+ await valkey.publish(keys.guiTakeoverChannel(), JSON.stringify({ port: state.port }));
77
+ }
78
+ catch {
79
+ // Best-effort, connection may already be closing
80
+ }
81
+ }
42
82
  await deregisterAgent(config.agentName);
43
83
  await disconnect();
44
84
  process.exit(0);
package/dist/valkey.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare const keys: {
9
9
  broadcastCursor: (agent: string) => string;
10
10
  notifyChannel: (agent: string) => string;
11
11
  eventsChannel: () => string;
12
+ guiTakeoverChannel: () => string;
12
13
  };
13
14
  export declare function connect(): Promise<void>;
14
15
  export declare function disconnect(): Promise<void>;
package/dist/valkey.js CHANGED
@@ -20,6 +20,7 @@ export const keys = {
20
20
  broadcastCursor: (agent) => `${prefix}bcursor:${agent}`,
21
21
  notifyChannel: (agent) => `${prefix}notify:${agent}`,
22
22
  eventsChannel: () => `${prefix}events`,
23
+ guiTakeoverChannel: () => `${prefix}gui:takeover`,
23
24
  };
24
25
  export async function connect() {
25
26
  await valkey.connect();
package/dist/web/api.js CHANGED
@@ -101,7 +101,9 @@ function ensureEventSubscription() {
101
101
  valkeySub.subscribe(keys.eventsChannel()).catch(() => {
102
102
  subscribedToEvents = false;
103
103
  });
104
- valkeySub.on("message", (_channel, message) => {
104
+ valkeySub.on("message", (channel, message) => {
105
+ if (channel !== keys.eventsChannel())
106
+ return;
105
107
  for (const client of sseClients) {
106
108
  try {
107
109
  client.send(message);
@@ -1 +1,7 @@
1
+ export interface GuiState {
2
+ running: boolean;
3
+ port: number | null;
4
+ }
5
+ export declare function getGuiState(): GuiState;
1
6
  export declare function startWebServer(port: number, attempt?: number): void;
7
+ export declare function restartOnPort(port: number): Promise<boolean>;
@@ -49,9 +49,16 @@ app.get("/", (c) => {
49
49
  return c.html(html);
50
50
  });
51
51
  const MAX_PORT_ATTEMPTS = 10;
52
+ let currentServer = null;
53
+ let currentPort = null;
54
+ export function getGuiState() {
55
+ return { running: currentServer !== null, port: currentPort };
56
+ }
52
57
  export function startWebServer(port, attempt = 0) {
53
58
  try {
54
59
  const server = serve({ fetch: app.fetch, port }, () => {
60
+ currentServer = server;
61
+ currentPort = port;
55
62
  process.stderr.write(`postino GUI: http://localhost:${port}\n`);
56
63
  });
57
64
  server.on("error", (err) => {
@@ -69,3 +76,45 @@ export function startWebServer(port, attempt = 0) {
69
76
  process.stderr.write(`postino GUI failed to start: ${err}\n`);
70
77
  }
71
78
  }
79
+ export function restartOnPort(port) {
80
+ return new Promise((resolve) => {
81
+ const oldServer = currentServer;
82
+ const oldPort = currentPort;
83
+ const launchNew = () => {
84
+ try {
85
+ const server = serve({ fetch: app.fetch, port }, () => {
86
+ currentServer = server;
87
+ currentPort = port;
88
+ process.stderr.write(`postino GUI: takeover http://localhost:${port}\n`);
89
+ resolve(true);
90
+ });
91
+ server.on("error", (err) => {
92
+ if (err.code === "EADDRINUSE") {
93
+ process.stderr.write(`postino: takeover port ${port} already claimed\n`);
94
+ }
95
+ else {
96
+ process.stderr.write(`postino: takeover failed: ${err.message}\n`);
97
+ }
98
+ // Restore old state if we had one
99
+ currentServer = oldServer;
100
+ currentPort = oldPort;
101
+ resolve(false);
102
+ });
103
+ }
104
+ catch {
105
+ currentServer = oldServer;
106
+ currentPort = oldPort;
107
+ resolve(false);
108
+ }
109
+ };
110
+ if (oldServer) {
111
+ // Close existing server first, then start on the new port
112
+ currentServer = null;
113
+ currentPort = null;
114
+ oldServer.close(() => launchNew());
115
+ }
116
+ else {
117
+ launchNew();
118
+ }
119
+ });
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manuelfedele/postino",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Inter-agent messaging and broadcast system for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",