@openlap/openlap 1.0.1 → 1.1.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.
@@ -0,0 +1,53 @@
1
+ interface ChannelUpdate {
2
+ id: string;
3
+ session_id?: string;
4
+ project_name: string;
5
+ display_name?: string;
6
+ body: string;
7
+ health: string;
8
+ created_at: string;
9
+ }
10
+ interface PresenceEvent {
11
+ action: "join" | "leave" | "timeout";
12
+ display_name: string;
13
+ channel: string;
14
+ }
15
+ interface JoinResponse {
16
+ channel: string;
17
+ present: string[];
18
+ recent: Array<{
19
+ id: string;
20
+ project_name?: string;
21
+ body: string;
22
+ health: string;
23
+ created_at: string;
24
+ }>;
25
+ }
26
+ type UpdateCallback = (channel: string, update: ChannelUpdate) => void;
27
+ type PresenceCallback = (channel: string, event: PresenceEvent) => void;
28
+ export declare class ChannelManager {
29
+ private baseUrl;
30
+ private channels;
31
+ private seenIds;
32
+ private onUpdate;
33
+ private onPresence;
34
+ private getToken;
35
+ private mySessionId;
36
+ private heartbeatInterval;
37
+ constructor(baseUrl: string, onUpdate: UpdateCallback, getToken: () => string | undefined, onPresence?: PresenceCallback);
38
+ setSessionId(sessionId: string): void;
39
+ join(channel: string, name?: string): Promise<JoinResponse | null>;
40
+ leave(channel: string): Promise<void>;
41
+ mute(channel: string): boolean;
42
+ focus(channel: string): boolean;
43
+ getFocusedChannel(): string | undefined;
44
+ isJoined(channel: string): boolean;
45
+ isMuted(channel: string): boolean;
46
+ private connectSSE;
47
+ private readStream;
48
+ private handleSSEEvent;
49
+ private startHeartbeat;
50
+ private stopHeartbeat;
51
+ stop(): void;
52
+ }
53
+ export {};
@@ -0,0 +1,256 @@
1
+ // v2: ChannelManager replaces FeedManager.
2
+ // Adds: join/leave via server API, mute/focus as local state,
3
+ // heartbeat loop, echo suppression by session_id.
4
+ export class ChannelManager {
5
+ baseUrl;
6
+ channels = new Map();
7
+ seenIds = new Set();
8
+ onUpdate;
9
+ onPresence;
10
+ getToken;
11
+ mySessionId;
12
+ heartbeatInterval = null;
13
+ constructor(baseUrl, onUpdate, getToken, onPresence) {
14
+ this.baseUrl = baseUrl;
15
+ this.onUpdate = onUpdate;
16
+ this.onPresence = onPresence;
17
+ this.getToken = getToken;
18
+ }
19
+ setSessionId(sessionId) {
20
+ this.mySessionId = sessionId;
21
+ }
22
+ // join calls the server API to register presence, then opens SSE.
23
+ async join(channel, name) {
24
+ if (this.channels.has(channel))
25
+ return null;
26
+ const token = this.getToken();
27
+ const headers = { "Content-Type": "application/json" };
28
+ if (token)
29
+ headers["Authorization"] = `Bearer ${token}`;
30
+ try {
31
+ const res = await fetch(`${this.baseUrl}/api/channels/${encodeURIComponent(channel)}/join`, {
32
+ method: "POST",
33
+ headers,
34
+ body: JSON.stringify({ name: name ?? undefined }),
35
+ });
36
+ if (!res.ok) {
37
+ process.stderr.write(`[openlap] join ${channel} failed: ${res.status}\n`);
38
+ return null;
39
+ }
40
+ const data = (await res.json());
41
+ process.stderr.write(`[openlap] joined ${channel} (${data.present?.length ?? 0} present)\n`);
42
+ // Start SSE subscription
43
+ const controller = new AbortController();
44
+ this.channels.set(channel, { controller, muted: false, focused: false });
45
+ this.connectSSE(channel, controller);
46
+ // Start heartbeat if first channel
47
+ if (this.channels.size === 1) {
48
+ this.startHeartbeat();
49
+ }
50
+ return data;
51
+ }
52
+ catch (err) {
53
+ process.stderr.write(`[openlap] join ${channel} error: ${err}\n`);
54
+ return null;
55
+ }
56
+ }
57
+ // leave calls the server API to remove presence, then closes SSE.
58
+ async leave(channel) {
59
+ const state = this.channels.get(channel);
60
+ if (!state)
61
+ return;
62
+ state.controller.abort();
63
+ this.channels.delete(channel);
64
+ // Stop heartbeat if no channels
65
+ if (this.channels.size === 0) {
66
+ this.stopHeartbeat();
67
+ }
68
+ const token = this.getToken();
69
+ const headers = {};
70
+ if (token)
71
+ headers["Authorization"] = `Bearer ${token}`;
72
+ try {
73
+ await fetch(`${this.baseUrl}/api/channels/${encodeURIComponent(channel)}/leave`, {
74
+ method: "POST",
75
+ headers,
76
+ });
77
+ process.stderr.write(`[openlap] left ${channel}\n`);
78
+ }
79
+ catch {
80
+ // best effort
81
+ }
82
+ }
83
+ // mute stops forwarding notifications but stays present (heartbeat continues).
84
+ mute(channel) {
85
+ const state = this.channels.get(channel);
86
+ if (!state)
87
+ return false;
88
+ state.muted = true;
89
+ process.stderr.write(`[openlap] muted ${channel}\n`);
90
+ return true;
91
+ }
92
+ // focus unmutes a channel and sets it as the focused channel for briefings.
93
+ focus(channel) {
94
+ const state = this.channels.get(channel);
95
+ if (!state)
96
+ return false;
97
+ // Unmute the focused channel
98
+ state.focused = true;
99
+ state.muted = false;
100
+ // Unfocus all others (only one focused at a time)
101
+ for (const [name, s] of this.channels) {
102
+ if (name !== channel)
103
+ s.focused = false;
104
+ }
105
+ process.stderr.write(`[openlap] focused ${channel}\n`);
106
+ return true;
107
+ }
108
+ getFocusedChannel() {
109
+ for (const [name, state] of this.channels) {
110
+ if (state.focused)
111
+ return name;
112
+ }
113
+ return undefined;
114
+ }
115
+ isJoined(channel) {
116
+ return this.channels.has(channel);
117
+ }
118
+ isMuted(channel) {
119
+ return this.channels.get(channel)?.muted ?? false;
120
+ }
121
+ // SSE connection for a channel
122
+ connectSSE(channel, controller) {
123
+ const url = `${this.baseUrl}/feed/${channel}/sse`;
124
+ const headers = {};
125
+ const token = this.getToken();
126
+ if (token)
127
+ headers["Authorization"] = `Bearer ${token}`;
128
+ fetch(url, { headers, signal: controller.signal })
129
+ .then((res) => {
130
+ if (!res.ok || !res.body)
131
+ throw new Error(`SSE connect failed: ${res.status}`);
132
+ return this.readStream(channel, res.body, controller);
133
+ })
134
+ .catch((err) => {
135
+ if (err.name === "AbortError")
136
+ return;
137
+ if (this.channels.has(channel)) {
138
+ setTimeout(() => this.connectSSE(channel, controller), 5000);
139
+ }
140
+ });
141
+ }
142
+ async readStream(channel, body, controller) {
143
+ const reader = body.getReader();
144
+ const decoder = new TextDecoder();
145
+ let buffer = "";
146
+ let eventType = "";
147
+ let eventData = "";
148
+ let eventId = "";
149
+ try {
150
+ while (true) {
151
+ const { done, value } = await reader.read();
152
+ if (done)
153
+ break;
154
+ buffer += decoder.decode(value, { stream: true });
155
+ const lines = buffer.split("\n");
156
+ buffer = lines.pop() ?? "";
157
+ for (const line of lines) {
158
+ if (line.startsWith("event: ")) {
159
+ eventType = line.slice(7);
160
+ }
161
+ else if (line.startsWith("data: ")) {
162
+ eventData = line.slice(6);
163
+ }
164
+ else if (line.startsWith("id: ")) {
165
+ eventId = line.slice(4);
166
+ }
167
+ else if (line === "") {
168
+ this.handleSSEEvent(channel, eventType, eventData, eventId);
169
+ eventType = "";
170
+ eventData = "";
171
+ eventId = "";
172
+ }
173
+ }
174
+ }
175
+ }
176
+ catch {
177
+ // stream ended
178
+ }
179
+ // Reconnect if still joined
180
+ if (this.channels.has(channel) && !controller.signal.aborted) {
181
+ setTimeout(() => this.connectSSE(channel, controller), 5000);
182
+ }
183
+ }
184
+ handleSSEEvent(channel, eventType, data, id) {
185
+ if (eventType === "update" && data && id) {
186
+ if (this.seenIds.has(id))
187
+ return;
188
+ this.seenIds.add(id);
189
+ // Cap seen set
190
+ if (this.seenIds.size > 500) {
191
+ const iter = this.seenIds.values();
192
+ for (let i = 0; i < 100; i++)
193
+ iter.next();
194
+ const toKeep = new Set();
195
+ for (const v of iter)
196
+ toKeep.add(v);
197
+ this.seenIds = toKeep;
198
+ }
199
+ try {
200
+ const update = JSON.parse(data);
201
+ // Echo suppression: skip own posts
202
+ if (this.mySessionId && update.session_id === this.mySessionId)
203
+ return;
204
+ // Mute check: skip if channel is muted
205
+ if (this.channels.get(channel)?.muted)
206
+ return;
207
+ this.onUpdate(channel, update);
208
+ }
209
+ catch {
210
+ // skip malformed
211
+ }
212
+ }
213
+ if (eventType === "presence" && data) {
214
+ try {
215
+ const event = JSON.parse(data);
216
+ event.channel = channel;
217
+ this.onPresence?.(channel, event);
218
+ }
219
+ catch {
220
+ // skip malformed
221
+ }
222
+ }
223
+ }
224
+ // Heartbeat: POST /api/presence/heartbeat every 30s
225
+ startHeartbeat() {
226
+ if (this.heartbeatInterval)
227
+ return;
228
+ this.heartbeatInterval = setInterval(async () => {
229
+ const token = this.getToken();
230
+ if (!token)
231
+ return;
232
+ try {
233
+ await fetch(`${this.baseUrl}/api/presence/heartbeat`, {
234
+ method: "POST",
235
+ headers: { "Authorization": `Bearer ${token}` },
236
+ });
237
+ }
238
+ catch {
239
+ // heartbeat failure is not critical
240
+ }
241
+ }, 30_000);
242
+ }
243
+ stopHeartbeat() {
244
+ if (this.heartbeatInterval) {
245
+ clearInterval(this.heartbeatInterval);
246
+ this.heartbeatInterval = null;
247
+ }
248
+ }
249
+ stop() {
250
+ this.stopHeartbeat();
251
+ for (const [, state] of this.channels) {
252
+ state.controller.abort();
253
+ }
254
+ this.channels.clear();
255
+ }
256
+ }
package/dist/index.js CHANGED
@@ -6,44 +6,37 @@ if (command === "setup") {
6
6
  console.log("Setting up openlap...");
7
7
  console.log("");
8
8
  try {
9
- // Add new "openlap" entry first (safe -- if this fails, old config still works)
10
- let added = false;
9
+ // Add MCP server entry
11
10
  try {
12
- execSync(`claude mcp add -s user openlap -- npx -y @openlap/openlap`, { stdio: "pipe" });
13
- added = true;
11
+ execSync(`claude mcp add -s user openlap -- openlap`, { stdio: "pipe" });
14
12
  console.log("Added openlap MCP server.");
15
13
  }
16
14
  catch (err) {
17
15
  const msg = err instanceof Error ? err.stderr?.toString() ?? err.message : String(err);
18
16
  if (msg.includes("already exists")) {
19
- console.log("openlap MCP server already installed.");
20
- added = true;
17
+ console.log("openlap MCP server already configured.");
21
18
  }
22
19
  else {
23
20
  throw err;
24
21
  }
25
22
  }
26
- // Only remove old entries after new one is confirmed
27
- if (added) {
28
- // Remove old "anylap" entry if present (migration from @openlap/anylap)
29
- try {
30
- execSync("claude mcp remove anylap", { stdio: "pipe" });
31
- console.log("Migrated: removed old 'anylap' entry.");
32
- }
33
- catch {
34
- // Not present, fine
35
- }
36
- // Remove old "lap" HTTP entry if present (migration from direct HTTP setup)
37
- try {
38
- execSync("claude mcp remove lap", { stdio: "pipe" });
39
- console.log("Migrated: removed old 'lap' HTTP entry.");
40
- }
41
- catch {
42
- // Not present, fine
43
- }
23
+ // Remove old entries
24
+ try {
25
+ execSync("claude mcp remove anylap", { stdio: "pipe" });
26
+ console.log("Migrated: removed old 'anylap' entry.");
27
+ }
28
+ catch { }
29
+ try {
30
+ execSync("claude mcp remove lap", { stdio: "pipe" });
31
+ console.log("Migrated: removed old 'lap' HTTP entry.");
44
32
  }
33
+ catch { }
34
+ console.log("");
35
+ console.log("Done. Start Claude Code with live feeds:");
36
+ console.log("");
37
+ console.log(" claude --dangerously-load-development-channels");
45
38
  console.log("");
46
- console.log("Done. Open Claude Code -- first message triggers GitHub login.");
39
+ console.log("First session triggers GitHub login in browser.");
47
40
  }
48
41
  catch {
49
42
  console.error("Failed to add MCP server. Is Claude Code installed?");
@@ -76,5 +69,26 @@ if (command && command !== "serve") {
76
69
  console.error("Run 'npx @openlap/openlap help' for usage.");
77
70
  process.exit(1);
78
71
  }
72
+ // -- Auto-update -------------------------------------------------------------
73
+ try {
74
+ const { readFileSync } = await import("fs");
75
+ const { join, dirname } = await import("path");
76
+ const { fileURLToPath } = await import("url");
77
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
78
+ const current = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
79
+ const latest = execSync("npm view @openlap/openlap version", {
80
+ encoding: "utf-8",
81
+ timeout: 5000,
82
+ stdio: ["pipe", "pipe", "pipe"],
83
+ }).trim();
84
+ if (latest && latest !== current) {
85
+ process.stderr.write(`[openlap] updating ${current} -> ${latest}\n`);
86
+ execSync("npm install -g @openlap/openlap@latest", { stdio: "pipe", timeout: 30000 });
87
+ process.stderr.write(`[openlap] updated to ${latest}\n`);
88
+ }
89
+ }
90
+ catch {
91
+ // not critical
92
+ }
79
93
  const { startProxy } = await import("./proxy.js");
80
94
  await startProxy();
package/dist/proxy.js CHANGED
@@ -8,6 +8,7 @@ import { join } from "path";
8
8
  import { homedir } from "os";
9
9
  import { FileOAuthProvider } from "./auth.js";
10
10
  import { detectProject, autoSave } from "./git.js";
11
+ import { ChannelManager } from "./channel.js";
11
12
  import { FeedManager } from "./feed.js";
12
13
  const BASE_URL = process.env.OPENLAP_URL ?? "https://openlap.app";
13
14
  // Tools that accept a 'project' parameter
@@ -15,22 +16,32 @@ const PROJECT_TOOLS = new Set([
15
16
  "list_laps", "get_lap", "create_lap", "save_lap", "update_lap",
16
17
  "post_update", "list_updates",
17
18
  "create_project", "update_project", "remove_project",
19
+ "check_lap", "post_channel",
18
20
  ]);
21
+ // v2 channel tools handled locally by the proxy
22
+ const PROXY_CHANNEL_TOOLS = new Set([
23
+ "join_channel", "leave_channel", "mute_channel", "focus_channel",
24
+ ]);
25
+ function getAuthToken() {
26
+ try {
27
+ const data = JSON.parse(readFileSync(join(homedir(), ".openlap", "auth.json"), "utf-8"));
28
+ return data?.tokens?.access_token;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
19
34
  export async function startProxy() {
20
35
  const authProvider = new FileOAuthProvider();
21
36
  // -- Connect to remote MCP server ----------------------------------------
22
37
  const remoteTransport = new StreamableHTTPClientTransport(new URL(`${BASE_URL}/mcp`), { authProvider });
23
38
  const remote = new Client({ name: "openlap-proxy", version: "1.0.0" }, { capabilities: {} });
24
- // Try connecting -- if auth is needed, the provider opens the browser
25
39
  try {
26
40
  await remote.connect(remoteTransport);
27
41
  }
28
42
  catch (err) {
29
- // After redirectToAuthorization, the SDK throws UnauthorizedError.
30
- // We have the auth code from the callback -- finish the flow.
31
43
  if (authProvider.authCode) {
32
44
  await remoteTransport.finishAuth(authProvider.authCode);
33
- // Retry connection
34
45
  await remote.connect(remoteTransport);
35
46
  }
36
47
  else {
@@ -39,7 +50,15 @@ export async function startProxy() {
39
50
  process.exit(1);
40
51
  }
41
52
  }
42
- // -- Auto-detect project from git remote ----------------------------------
53
+ // -- Version + project -------------------------------------------------------
54
+ try {
55
+ const { dirname } = await import("path");
56
+ const { fileURLToPath } = await import("url");
57
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
58
+ const ver = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
59
+ process.stderr.write(`[openlap] v${ver}\n`);
60
+ }
61
+ catch { }
43
62
  const detectedProject = detectProject();
44
63
  if (detectedProject) {
45
64
  process.stderr.write(`[openlap] project: ${detectedProject}\n`);
@@ -51,19 +70,19 @@ export async function startProxy() {
51
70
  experimental: { "claude/channel": {} },
52
71
  },
53
72
  });
54
- // -- Experimental: dynamic feed subscription ------------------------------
55
- const feeds = new FeedManager(BASE_URL, (tag, update) => {
73
+ // -- v2: ChannelManager ---------------------------------------------------
74
+ const channels = new ChannelManager(BASE_URL, (channel, update) => {
56
75
  const who = update.project_name || "unknown";
57
76
  const health = update.health && update.health !== "on_track" ? ` [${update.health}]` : "";
58
- const content = `[${who}]${health} ${update.body}`;
59
- process.stderr.write(`[openlap] feed ${tag}: ${content}\n`);
77
+ const displayName = update.display_name ? `${update.display_name}` : who;
78
+ const content = `[${channel}] ${displayName}${health}: ${update.body}`;
60
79
  server.notification({
61
80
  method: "notifications/claude/channel",
62
81
  params: {
63
82
  content,
64
83
  meta: {
65
84
  update_id: update.id,
66
- tag,
85
+ tag: channel,
67
86
  created_at: update.created_at,
68
87
  health: update.health ?? "on_track",
69
88
  },
@@ -73,16 +92,31 @@ export async function startProxy() {
73
92
  process.stderr.write(`[openlap] notification error: ${err}\n`);
74
93
  }
75
94
  });
76
- }, () => {
77
- // Provide auth token for SSE feed connections
78
- try {
79
- const data = JSON.parse(readFileSync(join(homedir(), ".openlap", "auth.json"), "utf-8"));
80
- return data?.tokens?.access_token;
81
- }
82
- catch {
83
- return undefined;
84
- }
95
+ }, getAuthToken, (channel, event) => {
96
+ // Presence events
97
+ const content = `[${channel}] ${event.display_name} ${event.action === "join" ? "joined" : event.action === "timeout" ? "timed out" : "left"}`;
98
+ server.notification({
99
+ method: "notifications/claude/channel",
100
+ params: { content, meta: { tag: channel, action: event.action } },
101
+ }).catch(() => { });
85
102
  });
103
+ // -- v1 compat: FeedManager for old track subscriptions -------------------
104
+ const feeds = new FeedManager(BASE_URL, (tag, update) => {
105
+ const who = update.project_name || "unknown";
106
+ const health = update.health && update.health !== "on_track" ? ` [${update.health}]` : "";
107
+ const content = `[${who}]${health} ${update.body}`;
108
+ server.notification({
109
+ method: "notifications/claude/channel",
110
+ params: {
111
+ content,
112
+ meta: { update_id: update.id, tag, created_at: update.created_at, health: update.health ?? "on_track" },
113
+ },
114
+ }).catch((err) => {
115
+ if (!String(err).includes("Not connected")) {
116
+ process.stderr.write(`[openlap] notification error: ${err}\n`);
117
+ }
118
+ });
119
+ }, getAuthToken);
86
120
  // Forward tool listing from remote
87
121
  server.setRequestHandler(ListToolsRequestSchema, async () => {
88
122
  const result = await remote.listTools();
@@ -92,28 +126,67 @@ export async function startProxy() {
92
126
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
93
127
  const { name } = req.params;
94
128
  const args = { ...(req.params.arguments ?? {}) };
95
- // Auto-inject project if not provided and detectable
129
+ // -- v2 proxy-side channel tools (handled locally) ----------------------
130
+ if (name === "join_channel") {
131
+ const channel = args.channel;
132
+ if (!channel)
133
+ return { content: [{ type: "text", text: "channel required" }], isError: true };
134
+ const result = await channels.join(channel, args.name);
135
+ return { content: [{ type: "text", text: JSON.stringify(result ?? { error: "already joined or failed" }) }] };
136
+ }
137
+ if (name === "leave_channel") {
138
+ const channel = args.channel;
139
+ if (!channel)
140
+ return { content: [{ type: "text", text: "channel required" }], isError: true };
141
+ await channels.leave(channel);
142
+ return { content: [{ type: "text", text: JSON.stringify({ status: "left", channel }) }] };
143
+ }
144
+ if (name === "mute_channel") {
145
+ const channel = args.channel;
146
+ if (!channel)
147
+ return { content: [{ type: "text", text: "channel required" }], isError: true };
148
+ const ok = channels.mute(channel);
149
+ return { content: [{ type: "text", text: ok ? `Muted ${channel}. Still present, notifications paused.` : `Not in ${channel}.` }] };
150
+ }
151
+ if (name === "focus_channel") {
152
+ const channel = args.channel;
153
+ if (!channel)
154
+ return { content: [{ type: "text", text: "channel required" }], isError: true };
155
+ const ok = channels.focus(channel);
156
+ return { content: [{ type: "text", text: ok ? `Focused on ${channel}. Unmuted and pinned to briefings.` : `Not in ${channel}.` }] };
157
+ }
158
+ // -- Auto-inject project ------------------------------------------------
96
159
  if (detectedProject && PROJECT_TOOLS.has(name) && !args.project) {
97
160
  args.project = detectedProject;
98
161
  }
99
- // Auto-save before post_update
100
- if (name === "post_update" && typeof args.body === "string") {
162
+ // -- Auto-inject channel from project for post_channel ------------------
163
+ if (name === "post_channel" && !args.channel && detectedProject) {
164
+ args.channel = detectedProject;
165
+ }
166
+ // -- Auto-save before post_update and post_channel ----------------------
167
+ if ((name === "post_update" || name === "post_channel") && typeof args.body === "string") {
101
168
  const saved = autoSave(args.body);
102
169
  if (saved) {
103
170
  process.stderr.write(`[openlap] auto-saved: ${saved}\n`);
104
171
  }
105
172
  }
106
- // Forward to remote
173
+ // -- Forward to remote --------------------------------------------------
107
174
  const result = await remote.callTool({ name, arguments: args });
108
- // Experimental: auto-subscribe to feeds from tool usage
175
+ // v1 compat: auto-subscribe to feeds from track tool usage
109
176
  if (typeof args.tag === "string" && args.tag) {
110
- feeds.subscribe(args.tag);
177
+ if (!channels.isJoined(args.tag)) {
178
+ feeds.subscribe(args.tag);
179
+ }
111
180
  }
112
181
  if (name === "get_track" && typeof args.name === "string" && args.name) {
113
- feeds.subscribe(args.name);
182
+ if (!channels.isJoined(args.name)) {
183
+ feeds.subscribe(args.name);
184
+ }
114
185
  }
115
186
  if (name === "create_track" && typeof args.name === "string" && args.name) {
116
- feeds.subscribe(args.name);
187
+ if (!channels.isJoined(args.name)) {
188
+ feeds.subscribe(args.name);
189
+ }
117
190
  }
118
191
  return {
119
192
  content: result.content,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlap/openlap",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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",