@openlap/openlap 1.1.2 → 1.1.4

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/channel.d.ts CHANGED
@@ -33,7 +33,6 @@ export declare class ChannelManager {
33
33
  private onPresence;
34
34
  private getToken;
35
35
  private mySessionId;
36
- private heartbeatInterval;
37
36
  constructor(baseUrl: string, onUpdate: UpdateCallback, getToken: () => string | undefined, onPresence?: PresenceCallback);
38
37
  setSessionId(sessionId: string): void;
39
38
  join(channel: string, name?: string): Promise<JoinResponse | null>;
@@ -43,6 +42,8 @@ export declare class ChannelManager {
43
42
  getFocusedChannel(): string | undefined;
44
43
  isJoined(channel: string): boolean;
45
44
  isMuted(channel: string): boolean;
45
+ hasChannels(): boolean;
46
+ heartbeat(): Promise<void>;
46
47
  private connectSSE;
47
48
  private readStream;
48
49
  private handleSSEEvent;
package/dist/channel.js CHANGED
@@ -9,7 +9,6 @@ export class ChannelManager {
9
9
  onPresence;
10
10
  getToken;
11
11
  mySessionId;
12
- heartbeatInterval = null;
13
12
  constructor(baseUrl, onUpdate, getToken, onPresence) {
14
13
  this.baseUrl = baseUrl;
15
14
  this.onUpdate = onUpdate;
@@ -39,6 +38,10 @@ export class ChannelManager {
39
38
  }
40
39
  const data = (await res.json());
41
40
  process.stderr.write(`[openlap] joined ${channel} (${data.present?.length ?? 0} present)\n`);
41
+ // Set session_id for echo suppression BEFORE opening SSE (fixes race condition)
42
+ if (data.session_id) {
43
+ this.mySessionId = data.session_id;
44
+ }
42
45
  // Start SSE subscription
43
46
  const controller = new AbortController();
44
47
  this.channels.set(channel, { controller, muted: false, focused: false });
@@ -118,6 +121,24 @@ export class ChannelManager {
118
121
  isMuted(channel) {
119
122
  return this.channels.get(channel)?.muted ?? false;
120
123
  }
124
+ hasChannels() {
125
+ return this.channels.size > 0;
126
+ }
127
+ // Public heartbeat -- called by proxy on every tool invocation.
128
+ async heartbeat() {
129
+ const token = this.getToken();
130
+ if (!token)
131
+ return;
132
+ try {
133
+ await fetch(`${this.baseUrl}/api/presence/heartbeat`, {
134
+ method: "POST",
135
+ headers: { "Authorization": `Bearer ${token}` },
136
+ });
137
+ }
138
+ catch {
139
+ // not critical
140
+ }
141
+ }
121
142
  // SSE connection for a channel
122
143
  connectSSE(channel, controller) {
123
144
  const url = `${this.baseUrl}/feed/${channel}/sse`;
@@ -221,38 +242,13 @@ export class ChannelManager {
221
242
  }
222
243
  }
223
244
  }
224
- // Heartbeat: POST /api/presence/heartbeat every 30s
245
+ // Immediate heartbeat on join. Ongoing heartbeats piggyback on tool calls
246
+ // (setInterval doesn't fire reliably in MCP stdio context).
225
247
  startHeartbeat() {
226
- if (this.heartbeatInterval)
227
- return;
228
- // Fire immediate heartbeat on first join
229
- const doHeartbeat = async () => {
230
- const token = this.getToken();
231
- if (!token) {
232
- process.stderr.write(`[openlap] heartbeat skipped: no token\n`);
233
- return;
234
- }
235
- try {
236
- const res = await fetch(`${this.baseUrl}/api/presence/heartbeat`, {
237
- method: "POST",
238
- headers: { "Authorization": `Bearer ${token}` },
239
- });
240
- if (!res.ok) {
241
- process.stderr.write(`[openlap] heartbeat failed: ${res.status}\n`);
242
- }
243
- }
244
- catch (err) {
245
- process.stderr.write(`[openlap] heartbeat error: ${err}\n`);
246
- }
247
- };
248
- doHeartbeat(); // immediate first heartbeat
249
- this.heartbeatInterval = setInterval(doHeartbeat, 30_000);
248
+ this.heartbeat().catch(() => { });
250
249
  }
251
250
  stopHeartbeat() {
252
- if (this.heartbeatInterval) {
253
- clearInterval(this.heartbeatInterval);
254
- this.heartbeatInterval = null;
255
- }
251
+ // No interval to clear -- heartbeats piggyback on tool calls
256
252
  }
257
253
  stop() {
258
254
  this.stopHeartbeat();
package/dist/proxy.js CHANGED
@@ -174,17 +174,29 @@ export async function startProxy() {
174
174
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
175
175
  const { name } = req.params;
176
176
  const args = { ...(req.params.arguments ?? {}) };
177
+ // -- Heartbeat on every tool call (keeps presence alive) ----------------
178
+ if (channels.hasChannels()) {
179
+ channels.heartbeat().catch(() => { });
180
+ }
177
181
  // -- v2 proxy-side channel tools (handled locally) ----------------------
178
182
  if (name === "join_channel") {
179
183
  const channel = args.channel;
180
184
  if (!channel)
181
185
  return { content: [{ type: "text", text: "channel required" }], isError: true };
182
186
  const result = await channels.join(channel, args.name);
183
- // Set session_id for echo suppression (returned by server on join)
184
- if (result && result.session_id) {
185
- channels.setSessionId(result.session_id);
186
- }
187
- return { content: [{ type: "text", text: JSON.stringify(result ?? { error: "already joined or failed" }) }] };
187
+ // session_id set internally by ChannelManager.join (before SSE opens)
188
+ if (!result)
189
+ return { content: [{ type: "text", text: JSON.stringify({ error: "already joined or failed" }) }] };
190
+ const r = result;
191
+ const lines = [`Joined ${channel}.`];
192
+ if (r.feed_url)
193
+ lines.push(`Feed: ${r.feed_url}`);
194
+ if (r.present?.length)
195
+ lines.push(`Present: ${r.present.join(", ")}`);
196
+ if (r.recent?.length)
197
+ lines.push(`Recent: ${r.recent.length} posts`);
198
+ lines.push("", JSON.stringify(result));
199
+ return { content: [{ type: "text", text: lines.join("\n") }] };
188
200
  }
189
201
  if (name === "leave_channel") {
190
202
  const channel = args.channel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlap/openlap",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Local MCP proxy for openlap.app -- auto-save, live feeds, project detection, one install",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",