@manuelfedele/postino 0.2.1 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { valkey, keys, publishEvent, getOnlineAgents, renameAgent } from "../valkey.js";
2
+ import { valkey, keys, publishEvent, getOnlineAgents, renameAgent, cleanupStaleAgents } from "../valkey.js";
3
3
  import { loadConfig } from "../types.js";
4
4
  const config = loadConfig();
5
5
  // Mutable agent identity so msg_rename can update it
@@ -273,4 +273,23 @@ export function registerMessagingTools(server, initialName) {
273
273
  }],
274
274
  };
275
275
  });
276
+ server.registerTool("msg_cleanup", {
277
+ title: "Cleanup Stale Agents",
278
+ description: [
279
+ "Remove offline agents with empty inboxes from the agents list.",
280
+ "Agents are auto-cleaned on shutdown, but this tool handles stragglers",
281
+ "(e.g. crashed processes that never ran their shutdown handler).",
282
+ ].join(" "),
283
+ inputSchema: {},
284
+ }, async () => {
285
+ const removed = await cleanupStaleAgents();
286
+ return {
287
+ content: [{
288
+ type: "text",
289
+ text: removed.length > 0
290
+ ? `Removed ${removed.length} stale agent(s): ${removed.join(", ")}`
291
+ : "No stale agents to clean up",
292
+ }],
293
+ };
294
+ });
276
295
  }
package/dist/valkey.d.ts CHANGED
@@ -16,5 +16,6 @@ export declare function disconnect(): Promise<void>;
16
16
  export declare function publishEvent(type: string, data: Record<string, unknown>): Promise<void>;
17
17
  export declare function registerAgent(name: string): Promise<void>;
18
18
  export declare function deregisterAgent(name: string): Promise<void>;
19
+ export declare function cleanupStaleAgents(): Promise<string[]>;
19
20
  export declare function renameAgent(oldName: string, newName: string): Promise<void>;
20
21
  export declare function getOnlineAgents(): Promise<string[]>;
package/dist/valkey.js CHANGED
@@ -61,8 +61,30 @@ export async function deregisterAgent(name) {
61
61
  if (heartbeatTimer)
62
62
  clearInterval(heartbeatTimer);
63
63
  await valkey.del(keys.agentInfo(name));
64
+ // Auto-cleanup: remove from agents set if inbox is empty
65
+ const inboxLen = await valkey.llen(keys.inbox(name));
66
+ if (inboxLen === 0) {
67
+ await valkey.srem(keys.agents(), name);
68
+ await valkey.del(keys.broadcastCursor(name));
69
+ }
64
70
  await publishEvent("agent_offline", { agent: name });
65
71
  }
72
+ export async function cleanupStaleAgents() {
73
+ const allAgents = await valkey.smembers(keys.agents());
74
+ const removed = [];
75
+ for (const name of allAgents) {
76
+ const online = await valkey.exists(keys.agentInfo(name));
77
+ if (online)
78
+ continue;
79
+ const inboxLen = await valkey.llen(keys.inbox(name));
80
+ if (inboxLen === 0) {
81
+ await valkey.srem(keys.agents(), name);
82
+ await valkey.del(keys.broadcastCursor(name));
83
+ removed.push(name);
84
+ }
85
+ }
86
+ return removed;
87
+ }
66
88
  export async function renameAgent(oldName, newName) {
67
89
  // Stop heartbeat for old name
68
90
  if (heartbeatTimer)
package/dist/web/api.js CHANGED
@@ -1,7 +1,22 @@
1
1
  import { Hono } from "hono";
2
2
  import { streamSSE } from "hono/streaming";
3
+ import { readFileSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import { valkey, valkeySub, keys, publishEvent, getOnlineAgents } from "../valkey.js";
4
7
  import { loadConfig } from "../types.js";
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ function readVersion() {
10
+ try {
11
+ const pkgPath = join(__dirname, "..", "package.json");
12
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
13
+ return pkg.version || "0.0.0";
14
+ }
15
+ catch {
16
+ return "0.0.0";
17
+ }
18
+ }
19
+ const VERSION = readVersion();
5
20
  const config = loadConfig();
6
21
  export const api = new Hono();
7
22
  // --- Agents ---
@@ -80,7 +95,7 @@ api.get("/stats", async (c) => {
80
95
  messageCount += await valkey.llen(keys.inbox(a));
81
96
  }
82
97
  const broadcastCount = await valkey.llen(keys.broadcasts());
83
- return c.json({ agentCount: agents.length, messageCount, broadcastCount });
98
+ return c.json({ version: VERSION, agentCount: agents.length, messageCount, broadcastCount });
84
99
  });
85
100
  // --- Agent-specific check (for hooks, zero-token) ---
86
101
  api.get("/check/:agent", async (c) => {
@@ -141,7 +141,7 @@
141
141
  <div class="topbar">
142
142
  <div class="logo">
143
143
  <h1>postino</h1>
144
- <span class="pill pill-muted">v0.1.0</span>
144
+ <span class="pill pill-muted" id="version"></span>
145
145
  <span class="pill pill-accent" id="agent-id"></span>
146
146
  </div>
147
147
  <div class="meta">
@@ -288,7 +288,7 @@ function connectSSE(){
288
288
  es.onerror=()=>{d.className='dot dot-off';t.textContent='reconnecting'};
289
289
  }
290
290
 
291
- async function loadStats(){const s=await api('/stats');if(!s)return;document.getElementById('stat-agents').textContent=s.agentCount+' agents';document.getElementById('stat-msg').textContent=s.messageCount+' msg';document.getElementById('stat-bc').textContent=s.broadcastCount+' bc'}
291
+ async function loadStats(){const s=await api('/stats');if(!s)return;document.getElementById('version').textContent='v'+s.version;document.getElementById('stat-agents').textContent=s.agentCount+' agents';document.getElementById('stat-msg').textContent=s.messageCount+' msg';document.getElementById('stat-bc').textContent=s.broadcastCount+' bc'}
292
292
 
293
293
  function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
294
294
  function fmtT(i){try{const d=new Date(i),n=new Date();return d.toDateString()===n.toDateString()?d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}):d.toLocaleDateString([],{month:'short',day:'numeric'})+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}catch{return i}}
@@ -78,43 +78,31 @@ export function startWebServer(port, attempt = 0) {
78
78
  }
79
79
  export function restartOnPort(port) {
80
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;
81
+ try {
82
+ const server = serve({ fetch: app.fetch, port }, () => {
83
+ // New server started successfully, close the old one
84
+ const oldServer = currentServer;
85
+ currentServer = server;
86
+ currentPort = port;
87
+ process.stderr.write(`postino GUI: takeover http://localhost:${port}\n`);
88
+ if (oldServer) {
89
+ oldServer.close();
90
+ }
91
+ resolve(true);
92
+ });
93
+ server.on("error", (err) => {
94
+ if (err.code === "EADDRINUSE") {
95
+ process.stderr.write(`postino: takeover port ${port} already claimed\n`);
96
+ }
97
+ else {
98
+ process.stderr.write(`postino: takeover failed: ${err.message}\n`);
99
+ }
100
+ // Old server stays running, state unchanged
107
101
  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());
102
+ });
115
103
  }
116
- else {
117
- launchNew();
104
+ catch {
105
+ resolve(false);
118
106
  }
119
107
  });
120
108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manuelfedele/postino",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Inter-agent messaging and broadcast system for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",