@openlap/openlap 1.1.2 → 1.1.3

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
@@ -180,11 +180,19 @@ export async function startProxy() {
180
180
  if (!channel)
181
181
  return { content: [{ type: "text", text: "channel required" }], isError: true };
182
182
  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" }) }] };
183
+ // session_id set internally by ChannelManager.join (before SSE opens)
184
+ if (!result)
185
+ return { content: [{ type: "text", text: JSON.stringify({ error: "already joined or failed" }) }] };
186
+ const r = result;
187
+ const lines = [`Joined ${channel}.`];
188
+ if (r.feed_url)
189
+ lines.push(`Feed: ${r.feed_url}`);
190
+ if (r.present?.length)
191
+ lines.push(`Present: ${r.present.join(", ")}`);
192
+ if (r.recent?.length)
193
+ lines.push(`Recent: ${r.recent.length} posts`);
194
+ lines.push("", JSON.stringify(result));
195
+ return { content: [{ type: "text", text: lines.join("\n") }] };
188
196
  }
189
197
  if (name === "leave_channel") {
190
198
  const channel = args.channel;
@@ -222,6 +230,10 @@ export async function startProxy() {
222
230
  process.stderr.write(`[openlap] auto-saved: ${saved}\n`);
223
231
  }
224
232
  }
233
+ // -- Heartbeat on every tool call (replaces unreliable setInterval) ------
234
+ if (channels.hasChannels()) {
235
+ channels.heartbeat().catch(() => { });
236
+ }
225
237
  // -- Forward to remote --------------------------------------------------
226
238
  const result = await remote.callTool({ name, arguments: args });
227
239
  // v1 compat: auto-subscribe to feeds from track tool usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlap/openlap",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
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",