burn-mcp-server 2.0.5 → 2.0.7

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
@@ -1,1066 +1,1108 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
- const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
- const supabase_js_1 = require("@supabase/supabase-js");
7
- const zod_1 = require("zod");
8
- // ---------------------------------------------------------------------------
9
- // Config
10
- // ---------------------------------------------------------------------------
11
- const SUPABASE_URL = process.env.BURN_SUPABASE_URL || 'https://juqtxylquemiuvvmgbej.supabase.co';
12
- const SUPABASE_ANON_KEY = process.env.BURN_SUPABASE_ANON_KEY || 'sb_publishable_reVgmmCC6ndIo6jFRMM2LQ_wujj5FrO';
13
- // Support both old JWT token (BURN_SUPABASE_TOKEN) and new long-lived MCP token (BURN_MCP_TOKEN)
14
- const MCP_TOKEN = process.env.BURN_MCP_TOKEN;
15
- const LEGACY_JWT = process.env.BURN_SUPABASE_TOKEN;
16
- const EXCHANGE_URL = process.env.BURN_MCP_EXCHANGE_URL || 'https://api.burn451.cloud/api/mcp-exchange';
17
- if (!MCP_TOKEN && !LEGACY_JWT) {
18
- console.error('Error: BURN_MCP_TOKEN environment variable is required.');
19
- console.error('Get your token from: Burn App → Settings → MCP Server → Generate Token');
20
- process.exit(1);
21
- }
22
- // ---------------------------------------------------------------------------
23
- // Supabase client — bootstrapped with anon key, session set after auth below
24
- // ---------------------------------------------------------------------------
25
- const supabase = (0, supabase_js_1.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY, {
26
- auth: {
27
- persistSession: false,
28
- autoRefreshToken: true,
29
- },
30
- ...(LEGACY_JWT ? { global: { headers: { Authorization: `Bearer ${LEGACY_JWT}` } } } : {}),
31
- });
32
- // ---------------------------------------------------------------------------
33
- // Auth: exchange MCP token for a real Supabase session (auto-refreshes)
34
- // Caches session locally so exchange is only needed on first run or token expiry
35
- // ---------------------------------------------------------------------------
36
- const node_os_1 = require("node:os");
37
- const node_fs_1 = require("node:fs");
38
- const node_path_1 = require("node:path");
39
- const SESSION_CACHE_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.burn');
40
- const SESSION_CACHE_FILE = (0, node_path_1.join)(SESSION_CACHE_DIR, 'mcp-session.json');
41
- function loadCachedSession() {
2
+
3
+ // src/index.ts
4
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5
+ var import_supabase_js = require("@supabase/supabase-js");
6
+
7
+ // src/lib/auth.ts
8
+ var DEFAULT_EXCHANGE_URL = "https://api.burn451.cloud/api/mcp-exchange";
9
+ async function exchangeToken(mcpToken, exchangeUrl = process.env.BURN_MCP_EXCHANGE_URL || DEFAULT_EXCHANGE_URL) {
10
+ const resp = await fetch(exchangeUrl, {
11
+ method: "POST",
12
+ headers: { "Content-Type": "application/json" },
13
+ body: JSON.stringify({ token: mcpToken })
14
+ });
15
+ if (!resp.ok) {
16
+ let detail = "";
42
17
  try {
43
- const raw = (0, node_fs_1.readFileSync)(SESSION_CACHE_FILE, 'utf-8');
44
- const data = JSON.parse(raw);
45
- if (data.access_token && data.refresh_token)
46
- return data;
18
+ detail = JSON.stringify(await resp.json());
19
+ } catch {
47
20
  }
48
- catch { /* no cache or invalid */ }
49
- return null;
21
+ throw new Error(`Token exchange failed (${resp.status}): ${detail}`);
22
+ }
23
+ const data = await resp.json();
24
+ if (!data.access_token || !data.refresh_token) {
25
+ throw new Error("Token exchange succeeded but returned no session tokens");
26
+ }
27
+ return { access_token: data.access_token, refresh_token: data.refresh_token };
50
28
  }
51
- function saveCachedSession(access_token, refresh_token) {
52
- try {
53
- (0, node_fs_1.mkdirSync)(SESSION_CACHE_DIR, { recursive: true });
54
- (0, node_fs_1.writeFileSync)(SESSION_CACHE_FILE, JSON.stringify({ access_token, refresh_token }), { mode: 0o600 });
55
- }
56
- catch { /* non-fatal — next startup will re-exchange */ }
29
+ async function applySession(supabase, session, onRefresh) {
30
+ const { error } = await supabase.auth.setSession(session);
31
+ if (error) throw new Error(`Failed to set session: ${error.message}`);
32
+ if (onRefresh) {
33
+ supabase.auth.onAuthStateChange((_event, s) => {
34
+ if (s?.access_token && s?.refresh_token) {
35
+ onRefresh({ access_token: s.access_token, refresh_token: s.refresh_token });
36
+ }
37
+ });
38
+ }
57
39
  }
58
- async function initAuth() {
59
- if (LEGACY_JWT)
60
- return; // legacy mode: JWT already set in headers above
61
- // Step 1: Try cached session (avoids network call on every restart)
62
- const cached = loadCachedSession();
63
- if (cached) {
64
- const { error } = await supabase.auth.setSession(cached);
65
- if (!error) {
66
- // Listen for token refresh so we keep the cache fresh
67
- supabase.auth.onAuthStateChange((_event, session) => {
68
- if (session?.access_token && session?.refresh_token) {
69
- saveCachedSession(session.access_token, session.refresh_token);
70
- }
71
- });
72
- console.error('Burn MCP: restored session from cache (no network needed)');
73
- return;
74
- }
75
- console.error('Burn MCP: cached session expired, re-exchanging...');
76
- }
77
- // Step 2: Exchange MCP token for a fresh Supabase session via Vercel API
78
- try {
79
- const resp = await fetch(EXCHANGE_URL, {
80
- method: 'POST',
81
- headers: { 'Content-Type': 'application/json' },
82
- body: JSON.stringify({ token: MCP_TOKEN }),
83
- });
84
- if (!resp.ok) {
85
- const body = await resp.json().catch(() => ({}));
86
- console.error(`Error: Token exchange failed (${resp.status}): ${body.error || 'Unknown'}`);
87
- console.error('Tokens expire after 30 days. Generate a new one in Burn App → Settings → MCP Server.');
88
- process.exit(1);
89
- }
90
- const { access_token, refresh_token } = await resp.json();
91
- const { error: sessionError } = await supabase.auth.setSession({
92
- access_token,
93
- refresh_token,
94
- });
95
- if (sessionError) {
96
- console.error('Error: Failed to set session.', sessionError.message);
97
- process.exit(1);
98
- }
99
- // Cache for next startup + listen for refresh
100
- saveCachedSession(access_token, refresh_token);
101
- supabase.auth.onAuthStateChange((_event, session) => {
102
- if (session?.access_token && session?.refresh_token) {
103
- saveCachedSession(session.access_token, session.refresh_token);
104
- }
105
- });
106
- console.error('Burn MCP: session exchanged and cached');
107
- }
108
- catch (err) {
109
- console.error('Error: Could not reach token exchange endpoint.', err.message);
110
- console.error(`URL: ${EXCHANGE_URL}`);
111
- process.exit(1);
112
- }
40
+ var HTTP_CACHE_TTL_MS = 5 * 6e4;
41
+
42
+ // src/lib/auth-stdio.ts
43
+ var import_node_os = require("node:os");
44
+ var import_node_fs = require("node:fs");
45
+ var import_node_path = require("node:path");
46
+ var CACHE_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".burn");
47
+ var CACHE_FILE = (0, import_node_path.join)(CACHE_DIR, "mcp-session.json");
48
+ function loadCachedSession() {
49
+ try {
50
+ const raw = (0, import_node_fs.readFileSync)(CACHE_FILE, "utf-8");
51
+ const data = JSON.parse(raw);
52
+ if (data.access_token && data.refresh_token) return data;
53
+ } catch {
54
+ }
55
+ return null;
113
56
  }
114
- // ---------------------------------------------------------------------------
115
- // Rate limiter — simple sliding window (per MCP session, in-memory)
116
- // ---------------------------------------------------------------------------
117
- const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
118
- const RATE_LIMIT_MAX_CALLS = 30; // max tool calls per window
119
- const rateLimitLog = [];
120
- function checkRateLimit() {
57
+ function saveCachedSession(session) {
58
+ try {
59
+ (0, import_node_fs.mkdirSync)(CACHE_DIR, { recursive: true });
60
+ (0, import_node_fs.writeFileSync)(CACHE_FILE, JSON.stringify(session), { mode: 384 });
61
+ } catch {
62
+ }
63
+ }
64
+
65
+ // src/setup.ts
66
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
67
+ var import_zod = require("zod");
68
+ function createBurnServer(supabase, opts = {}) {
69
+ const RATE_LIMIT_WINDOW_MS = 6e4;
70
+ const RATE_LIMIT_MAX_CALLS = opts.rateLimitPerMin ?? 30;
71
+ const rateLimitLog = [];
72
+ function checkRateLimit() {
73
+ if (RATE_LIMIT_MAX_CALLS === 0) return null;
121
74
  const now = Date.now();
122
- // Remove entries outside the window
123
75
  while (rateLimitLog.length > 0 && rateLimitLog[0] < now - RATE_LIMIT_WINDOW_MS) {
124
- rateLimitLog.shift();
76
+ rateLimitLog.shift();
125
77
  }
126
78
  if (rateLimitLog.length >= RATE_LIMIT_MAX_CALLS) {
127
- const retryAfter = Math.ceil((rateLimitLog[0] + RATE_LIMIT_WINDOW_MS - now) / 1000);
128
- return `Rate limit exceeded (${RATE_LIMIT_MAX_CALLS} calls/min). Retry after ${retryAfter}s.`;
79
+ const retryAfter = Math.ceil((rateLimitLog[0] + RATE_LIMIT_WINDOW_MS - now) / 1e3);
80
+ return `Rate limit exceeded (${RATE_LIMIT_MAX_CALLS} calls/min). Retry after ${retryAfter}s.`;
129
81
  }
130
82
  rateLimitLog.push(now);
131
83
  return null;
132
- }
133
- // ---------------------------------------------------------------------------
134
- // MCP Server
135
- // ---------------------------------------------------------------------------
136
- const server = new mcp_js_1.McpServer({
137
- name: 'burn-mcp-server',
138
- version: '2.0.0',
139
- });
140
- // ---------------------------------------------------------------------------
141
- // Helper: standard text result
142
- // ---------------------------------------------------------------------------
143
- function textResult(text) {
144
- return { content: [{ type: 'text', text }] };
145
- }
146
- // ---------------------------------------------------------------------------
147
- // Helper: verify bookmark exists and has expected status
148
- // ---------------------------------------------------------------------------
149
- async function verifyBookmark(id, expectedStatus) {
150
- const { data, error } = await supabase
151
- .from('bookmarks')
152
- .select('*')
153
- .eq('id', id)
154
- .single();
155
- if (error)
156
- return { data: null, error: error.code === 'PGRST116' ? 'Bookmark not found' : error.message };
84
+ }
85
+ const server = new import_mcp.McpServer({
86
+ name: opts.name || "burn-mcp-server",
87
+ version: opts.version || "2.0.7"
88
+ });
89
+ function textResult(text) {
90
+ return { content: [{ type: "text", text }] };
91
+ }
92
+ async function verifyBookmark(id, expectedStatus) {
93
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("id", id).single();
94
+ if (error) return { data: null, error: error.code === "PGRST116" ? "Bookmark not found" : error.message };
157
95
  if (expectedStatus) {
158
- const allowed = Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus];
159
- if (!allowed.includes(data.status)) {
160
- const statusLabels = { active: 'Flame', read: 'Spark', absorbed: 'Vault', ash: 'Ash' };
161
- return { data, error: `Bookmark is in ${statusLabels[data.status] || data.status} (expected ${allowed.map(s => statusLabels[s] || s).join(' or ')})` };
162
- }
96
+ const allowed = Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus];
97
+ if (!allowed.includes(data.status)) {
98
+ const statusLabels = { active: "Flame", read: "Spark", absorbed: "Vault", ash: "Ash" };
99
+ return { data, error: `Bookmark is in ${statusLabels[data.status] || data.status} (expected ${allowed.map((s) => statusLabels[s] || s).join(" or ")})` };
100
+ }
163
101
  }
164
102
  return { data, error: null };
165
- }
166
- // ---------------------------------------------------------------------------
167
- // Helper: merge fields into content_metadata JSONB without overwriting
168
- // ---------------------------------------------------------------------------
169
- async function mergeContentMetadata(bookmarkId, fields, extraColumns) {
170
- const { data, error } = await supabase
171
- .from('bookmarks')
172
- .select('content_metadata')
173
- .eq('id', bookmarkId)
174
- .single();
175
- if (error)
176
- return { error: error.message };
177
- const existing = (data.content_metadata || {});
178
- // Only merge non-undefined fields
103
+ }
104
+ async function mergeContentMetadata(bookmarkId, fields, extraColumns) {
105
+ const { data, error } = await supabase.from("bookmarks").select("content_metadata").eq("id", bookmarkId).single();
106
+ if (error) return { error: error.message };
107
+ const existing = data.content_metadata || {};
179
108
  const cleaned = {};
180
109
  for (const [k, v] of Object.entries(fields)) {
181
- if (v !== undefined && v !== null)
182
- cleaned[k] = v;
110
+ if (v !== void 0 && v !== null) cleaned[k] = v;
183
111
  }
184
112
  const merged = { ...existing, ...cleaned };
185
- const { error: updateError } = await supabase
186
- .from('bookmarks')
187
- .update({ content_metadata: merged, ...extraColumns })
188
- .eq('id', bookmarkId);
113
+ const { error: updateError } = await supabase.from("bookmarks").update({ content_metadata: merged, ...extraColumns }).eq("id", bookmarkId);
189
114
  return { error: updateError?.message || null };
190
- }
191
- // ---------------------------------------------------------------------------
192
- // Helper: extract fields from content_metadata JSONB
193
- // ---------------------------------------------------------------------------
194
- function meta(row) {
115
+ }
116
+ function meta(row) {
195
117
  const m = row.content_metadata || {};
196
118
  return {
197
- id: row.id,
198
- url: row.url,
199
- title: row.title,
200
- author: m.author || row.author || null,
201
- platform: row.platform,
202
- status: row.status,
203
- tags: m.tags || [],
204
- thumbnail: m.thumbnail || null,
205
- vaultCategory: m.vault_category || null,
206
- vaultedAt: m.vaulted_at || null,
207
- aiPositioning: m.ai_positioning || null,
208
- aiDensity: m.ai_density || null,
209
- aiMinutes: m.ai_minutes || null,
210
- aiTakeaway: m.ai_takeaway || [],
211
- aiStrategyReason: m.ai_strategy_reason || null,
212
- aiHowToRead: m.ai_how_to_read || null,
213
- aiOverlap: m.ai_overlap || null,
214
- aiVerdict: m.ai_verdict || null,
215
- aiSummary: m.ai_summary || null,
216
- sparkInsight: m.spark_insight || null,
217
- extractedContent: m.extracted_content || null,
218
- externalURL: m.external_url || null,
219
- aiRelevance: m.ai_relevance || null,
220
- aiNovelty: m.ai_novelty || null,
221
- createdAt: row.created_at,
222
- countdownExpiresAt: row.countdown_expires_at,
223
- readAt: row.read_at,
119
+ id: row.id,
120
+ url: row.url,
121
+ title: row.title,
122
+ author: m.author || row.author || null,
123
+ platform: row.platform,
124
+ status: row.status,
125
+ tags: m.tags || [],
126
+ thumbnail: m.thumbnail || null,
127
+ vaultCategory: m.vault_category || null,
128
+ vaultedAt: m.vaulted_at || null,
129
+ aiPositioning: m.ai_positioning || null,
130
+ aiDensity: m.ai_density || null,
131
+ aiMinutes: m.ai_minutes || null,
132
+ aiTakeaway: m.ai_takeaway || [],
133
+ aiStrategyReason: m.ai_strategy_reason || null,
134
+ aiHowToRead: m.ai_how_to_read || null,
135
+ aiOverlap: m.ai_overlap || null,
136
+ aiVerdict: m.ai_verdict || null,
137
+ aiSummary: m.ai_summary || null,
138
+ sparkInsight: m.spark_insight || null,
139
+ extractedContent: m.extracted_content || null,
140
+ externalURL: m.external_url || null,
141
+ aiRelevance: m.ai_relevance || null,
142
+ aiNovelty: m.ai_novelty || null,
143
+ createdAt: row.created_at,
144
+ countdownExpiresAt: row.countdown_expires_at,
145
+ readAt: row.read_at
224
146
  };
225
- }
226
- /** Compact summary for list views (no extracted content) */
227
- function metaSummary(row) {
147
+ }
148
+ function metaSummary(row) {
228
149
  const m = row.content_metadata || {};
229
150
  return {
230
- id: row.id,
231
- url: row.url,
232
- title: row.title,
233
- author: m.author || null,
234
- platform: row.platform,
235
- tags: m.tags || [],
236
- vaultCategory: m.vault_category || null,
237
- vaultedAt: m.vaulted_at || null,
238
- aiPositioning: m.ai_positioning || null,
239
- aiTakeaway: m.ai_takeaway || [],
151
+ id: row.id,
152
+ url: row.url,
153
+ title: row.title,
154
+ author: m.author || null,
155
+ platform: row.platform,
156
+ tags: m.tags || [],
157
+ vaultCategory: m.vault_category || null,
158
+ vaultedAt: m.vaulted_at || null,
159
+ aiPositioning: m.ai_positioning || null,
160
+ aiTakeaway: m.ai_takeaway || []
240
161
  };
241
- }
242
- /** Flame-specific summary with countdown and AI triage fields */
243
- function flameSummary(row) {
162
+ }
163
+ function flameSummary(row) {
244
164
  const m = row.content_metadata || {};
245
165
  const expiresAt = row.countdown_expires_at ? new Date(row.countdown_expires_at) : null;
246
- const now = new Date();
166
+ const now = /* @__PURE__ */ new Date();
247
167
  const remainingMs = expiresAt ? expiresAt.getTime() - now.getTime() : 0;
248
- const remainingHours = Math.max(0, Math.round(remainingMs / 3600000 * 10) / 10);
168
+ const remainingHours = Math.max(0, Math.round(remainingMs / 36e5 * 10) / 10);
249
169
  return {
250
- id: row.id,
251
- url: row.url,
252
- title: row.title,
253
- author: m.author || null,
254
- platform: row.platform,
255
- tags: m.tags || [],
256
- createdAt: row.created_at,
257
- expiresAt: row.countdown_expires_at,
258
- remainingHours,
259
- isBurning: remainingHours <= 6,
260
- isCritical: remainingHours <= 1,
261
- aiPositioning: m.ai_positioning || null,
262
- aiDensity: m.ai_density || null,
263
- aiMinutes: m.ai_minutes || null,
264
- aiTakeaway: m.ai_takeaway || [],
265
- aiStrategy: m.ai_strategy || null,
266
- aiStrategyReason: m.ai_strategy_reason || null,
267
- aiHowToRead: m.ai_how_to_read || null,
268
- aiRelevance: m.ai_relevance || null,
269
- aiNovelty: m.ai_novelty || null,
270
- aiOverlap: m.ai_overlap || null,
271
- aiHook: m.ai_hook || null,
272
- aiAbout: m.ai_about || [],
170
+ id: row.id,
171
+ url: row.url,
172
+ title: row.title,
173
+ author: m.author || null,
174
+ platform: row.platform,
175
+ tags: m.tags || [],
176
+ createdAt: row.created_at,
177
+ expiresAt: row.countdown_expires_at,
178
+ remainingHours,
179
+ isBurning: remainingHours <= 6,
180
+ isCritical: remainingHours <= 1,
181
+ aiPositioning: m.ai_positioning || null,
182
+ aiDensity: m.ai_density || null,
183
+ aiMinutes: m.ai_minutes || null,
184
+ aiTakeaway: m.ai_takeaway || [],
185
+ aiStrategy: m.ai_strategy || null,
186
+ aiStrategyReason: m.ai_strategy_reason || null,
187
+ aiHowToRead: m.ai_how_to_read || null,
188
+ aiRelevance: m.ai_relevance || null,
189
+ aiNovelty: m.ai_novelty || null,
190
+ aiOverlap: m.ai_overlap || null,
191
+ aiHook: m.ai_hook || null,
192
+ aiAbout: m.ai_about || []
273
193
  };
274
- }
275
- // ---------------------------------------------------------------------------
276
- // Tool handlers (all wrapped with rate limiting)
277
- // ---------------------------------------------------------------------------
278
- /** Wrap a handler with rate limiting */
279
- function rateLimited(handler) {
194
+ }
195
+ function rateLimited(handler) {
280
196
  return async (args) => {
281
- const err = checkRateLimit();
282
- if (err)
283
- return textResult(err);
284
- return handler(args);
197
+ const err = checkRateLimit();
198
+ if (err) return textResult(err);
199
+ return handler(args);
285
200
  };
286
- }
287
- async function handleSearchVault(args) {
201
+ }
202
+ async function handleSearchVault(args) {
288
203
  const { query, limit } = args;
289
- const { data, error } = await supabase
290
- .from('bookmarks')
291
- .select('*')
292
- .eq('status', 'absorbed')
293
- .ilike('title', `%${query}%`)
294
- .order('created_at', { ascending: false })
295
- .limit(limit || 10);
296
- if (error)
297
- return textResult(`Error: ${error.message}`);
298
- // Also search in content_metadata tags and takeaway
204
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("status", "absorbed").ilike("title", `%${query}%`).order("created_at", { ascending: false }).limit(limit || 10);
205
+ if (error) return textResult(`Error: ${error.message}`);
299
206
  let results = (data || []).map(metaSummary);
300
- // If title search returned few results, also search by tag
301
207
  if (results.length < (limit || 10)) {
302
- const { data: tagData } = await supabase
303
- .from('bookmarks')
304
- .select('*')
305
- .eq('status', 'absorbed')
306
- .order('created_at', { ascending: false })
307
- .limit(50);
308
- if (tagData) {
309
- const existingIds = new Set(results.map((r) => r.id));
310
- const tagMatches = tagData
311
- .filter((row) => {
312
- if (existingIds.has(row.id))
313
- return false;
314
- const m = row.content_metadata || {};
315
- const tags = (m.tags || []);
316
- const takeaway = (m.ai_takeaway || []);
317
- const positioning = m.ai_positioning || '';
318
- const allText = [...tags, ...takeaway, positioning].join(' ').toLowerCase();
319
- return allText.includes(query.toLowerCase());
320
- })
321
- .map(metaSummary);
322
- results = [...results, ...tagMatches].slice(0, limit || 10);
323
- }
208
+ const { data: tagData } = await supabase.from("bookmarks").select("*").eq("status", "absorbed").order("created_at", { ascending: false }).limit(50);
209
+ if (tagData) {
210
+ const existingIds = new Set(results.map((r) => r.id));
211
+ const tagMatches = tagData.filter((row) => {
212
+ if (existingIds.has(row.id)) return false;
213
+ const m = row.content_metadata || {};
214
+ const tags = m.tags || [];
215
+ const takeaway = m.ai_takeaway || [];
216
+ const positioning = m.ai_positioning || "";
217
+ const allText = [...tags, ...takeaway, positioning].join(" ").toLowerCase();
218
+ return allText.includes(query.toLowerCase());
219
+ }).map(metaSummary);
220
+ results = [...results, ...tagMatches].slice(0, limit || 10);
221
+ }
324
222
  }
325
223
  return textResult(JSON.stringify(results, null, 2));
326
- }
327
- async function handleGetBookmark(args) {
328
- const { data, error } = await supabase
329
- .from('bookmarks')
330
- .select('*')
331
- .eq('id', args.id)
332
- .single();
333
- if (error)
334
- return textResult(`Error: ${error.message}`);
224
+ }
225
+ async function handleGetBookmark(args) {
226
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("id", args.id).single();
227
+ if (error) return textResult(`Error: ${error.message}`);
335
228
  return textResult(JSON.stringify(meta(data), null, 2));
336
- }
337
- async function handleListCategories() {
338
- const { data, error } = await supabase
339
- .from('bookmarks')
340
- .select('content_metadata')
341
- .eq('status', 'absorbed');
342
- if (error)
343
- return textResult(`Error: ${error.message}`);
229
+ }
230
+ async function handleListCategories() {
231
+ const { data, error } = await supabase.from("bookmarks").select("content_metadata").eq("status", "absorbed");
232
+ if (error) return textResult(`Error: ${error.message}`);
344
233
  const counts = {};
345
234
  for (const row of data || []) {
346
- const cat = row.content_metadata?.vault_category || 'Uncategorized';
347
- counts[cat] = (counts[cat] || 0) + 1;
235
+ const cat = row.content_metadata?.vault_category || "Uncategorized";
236
+ counts[cat] = (counts[cat] || 0) + 1;
348
237
  }
349
- const categories = Object.entries(counts)
350
- .map(([category, count]) => ({ category, count }))
351
- .sort((a, b) => b.count - a.count);
238
+ const categories = Object.entries(counts).map(([category, count]) => ({ category, count })).sort((a, b) => b.count - a.count);
352
239
  return textResult(JSON.stringify(categories, null, 2));
353
- }
354
- async function handleGetCollections() {
355
- const { data, error } = await supabase
356
- .from('collections')
357
- .select('id, name, bookmark_ids, ai_overview');
358
- if (error)
359
- return textResult(`Error: ${error.message}`);
240
+ }
241
+ async function handleGetCollections() {
242
+ const { data, error } = await supabase.from("collections").select("id, name, bookmark_ids, ai_overview");
243
+ if (error) return textResult(`Error: ${error.message}`);
360
244
  const collections = (data || []).map((c) => ({
361
- id: c.id,
362
- name: c.name,
363
- articleCount: Array.isArray(c.bookmark_ids) ? c.bookmark_ids.length : 0,
364
- overview: c.ai_overview?.theme || null,
245
+ id: c.id,
246
+ name: c.name,
247
+ articleCount: Array.isArray(c.bookmark_ids) ? c.bookmark_ids.length : 0,
248
+ overview: c.ai_overview?.theme || null
365
249
  }));
366
250
  return textResult(JSON.stringify(collections, null, 2));
367
- }
368
- async function handleGetCollectionOverview(args) {
369
- const { data: collection, error } = await supabase
370
- .from('collections')
371
- .select('*')
372
- .eq('name', args.name)
373
- .single();
251
+ }
252
+ async function handleGetCollectionOverview(args) {
253
+ const { data: collection, error } = await supabase.from("collections").select("*").eq("name", args.name).single();
374
254
  if (error) {
375
- return textResult(error.code === 'PGRST116'
376
- ? `No collection found with name "${args.name}".`
377
- : `Error: ${error.message}`);
255
+ return textResult(
256
+ error.code === "PGRST116" ? `No collection found with name "${args.name}".` : `Error: ${error.message}`
257
+ );
378
258
  }
379
259
  let bookmarks = [];
380
260
  if (Array.isArray(collection.bookmark_ids) && collection.bookmark_ids.length > 0) {
381
- const { data: bData, error: bError } = await supabase
382
- .from('bookmarks')
383
- .select('*')
384
- .in('id', collection.bookmark_ids);
385
- if (!bError && bData) {
386
- bookmarks = bData.map(metaSummary);
387
- }
261
+ const { data: bData, error: bError } = await supabase.from("bookmarks").select("*").in("id", collection.bookmark_ids);
262
+ if (!bError && bData) {
263
+ bookmarks = bData.map(metaSummary);
264
+ }
388
265
  }
389
266
  return textResult(JSON.stringify({
390
- id: collection.id,
391
- name: collection.name,
392
- articleCount: Array.isArray(collection.bookmark_ids) ? collection.bookmark_ids.length : 0,
393
- aiOverview: collection.ai_overview,
394
- bookmarks,
267
+ id: collection.id,
268
+ name: collection.name,
269
+ articleCount: Array.isArray(collection.bookmark_ids) ? collection.bookmark_ids.length : 0,
270
+ aiOverview: collection.ai_overview,
271
+ bookmarks
395
272
  }, null, 2));
396
- }
397
- async function handleGetArticleContent(args) {
398
- const { data, error } = await supabase
399
- .from('bookmarks')
400
- .select('*')
401
- .eq('id', args.id)
402
- .single();
403
- if (error)
404
- return textResult(`Error: ${error.message}`);
273
+ }
274
+ async function handleGetArticleContent(args) {
275
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("id", args.id).single();
276
+ if (error) return textResult(`Error: ${error.message}`);
405
277
  return textResult(JSON.stringify(meta(data), null, 2));
406
- }
407
- // ---------------------------------------------------------------------------
408
- // Vercel API base URL for content fetching
409
- // ---------------------------------------------------------------------------
410
- const API_BASE = process.env.BURN_API_URL || 'https://api.burn451.cloud';
411
- const API_KEY = process.env.BURN_API_KEY || 'burn451-2026-secret-key';
412
- /** Detect platform from URL */
413
- function detectPlatform(url) {
414
- if (/x\.com|twitter\.com/i.test(url))
415
- return 'x';
416
- if (/youtube\.com|youtu\.be/i.test(url))
417
- return 'youtube';
418
- if (/reddit\.com|redd\.it/i.test(url))
419
- return 'reddit';
420
- if (/bilibili\.com|b23\.tv/i.test(url))
421
- return 'bilibili';
422
- if (/open\.spotify\.com/i.test(url))
423
- return 'spotify';
424
- if (/mp\.weixin\.qq\.com/i.test(url))
425
- return 'wechat';
426
- if (/xiaohongshu\.com|xhslink\.com/i.test(url))
427
- return 'xhs';
428
- return 'web';
429
- }
430
- /** Fetch content via Vercel API proxy (bypasses GFW for X.com, etc.) */
431
- async function fetchViaAPI(url, platform) {
278
+ }
279
+ const API_BASE = process.env.BURN_API_URL || "https://api.burn451.cloud";
280
+ const API_KEY = process.env.BURN_API_KEY;
281
+ function detectPlatform(url) {
282
+ if (/x\.com|twitter\.com/i.test(url)) return "x";
283
+ if (/youtube\.com|youtu\.be/i.test(url)) return "youtube";
284
+ if (/reddit\.com|redd\.it/i.test(url)) return "reddit";
285
+ if (/bilibili\.com|b23\.tv/i.test(url)) return "bilibili";
286
+ if (/open\.spotify\.com/i.test(url)) return "spotify";
287
+ if (/mp\.weixin\.qq\.com/i.test(url)) return "wechat";
288
+ if (/xiaohongshu\.com|xhslink\.com/i.test(url)) return "xhs";
289
+ return "web";
290
+ }
291
+ async function fetchViaAPI(url, platform) {
432
292
  try {
433
- let endpoint;
434
- let params;
435
- switch (platform) {
436
- case 'x':
437
- endpoint = `${API_BASE}/api/parse-x`;
438
- params = `url=${encodeURIComponent(url)}`;
439
- break;
440
- case 'reddit':
441
- endpoint = `${API_BASE}/api/parse-reddit`;
442
- params = `url=${encodeURIComponent(url)}`;
443
- break;
444
- case 'spotify':
445
- endpoint = `${API_BASE}/api/parse-meta`;
446
- params = `url=${encodeURIComponent(url)}&_platform=spotify`;
447
- break;
448
- case 'wechat':
449
- case 'xhs':
450
- endpoint = `${API_BASE}/api/parse-meta`;
451
- params = `url=${encodeURIComponent(url)}&_platform=${platform}`;
452
- break;
453
- case 'youtube':
454
- // Try transcript extraction via jina-extract with youtube platform hint
455
- endpoint = `${API_BASE}/api/jina-extract`;
456
- params = `url=${encodeURIComponent(url)}&_platform=youtube`;
457
- break;
458
- default:
459
- endpoint = `${API_BASE}/api/jina-extract`;
460
- params = `url=${encodeURIComponent(url)}`;
461
- break;
462
- }
463
- const resp = await fetch(`${endpoint}?${params}`, {
464
- headers: {
465
- 'x-api-key': API_KEY,
466
- 'Accept': 'application/json',
467
- },
468
- signal: AbortSignal.timeout(30000),
469
- });
470
- if (!resp.ok) {
471
- return { error: `API returned ${resp.status}` };
472
- }
473
- const data = await resp.json();
474
- // Normalize response across different API endpoints
475
- if (platform === 'x') {
476
- const text = data.text || data.article_text || '';
477
- const quoteText = data.quote ? `\n\n[Quote from @${data.quote.handle}]: ${data.quote.text}` : '';
478
- return {
479
- title: data.text?.slice(0, 100) || 'Tweet',
480
- author: data.author ? `@${data.handle || data.author}` : undefined,
481
- content: text + quoteText,
482
- };
483
- }
484
- if (platform === 'spotify') {
485
- return {
486
- title: data.title,
487
- author: data.author,
488
- content: data.extracted_content || data.description || null,
489
- };
490
- }
491
- if (platform === 'wechat') {
492
- // WeChat parse-meta returns extracted_content from js_content div
493
- return {
494
- title: data.title,
495
- author: data.author,
496
- content: data.extracted_content || data.content || null,
497
- };
498
- }
293
+ let endpoint;
294
+ let params;
295
+ switch (platform) {
296
+ case "x":
297
+ endpoint = `${API_BASE}/api/parse-x`;
298
+ params = `url=${encodeURIComponent(url)}`;
299
+ break;
300
+ case "reddit":
301
+ endpoint = `${API_BASE}/api/parse-reddit`;
302
+ params = `url=${encodeURIComponent(url)}`;
303
+ break;
304
+ case "spotify":
305
+ endpoint = `${API_BASE}/api/parse-meta`;
306
+ params = `url=${encodeURIComponent(url)}&_platform=spotify`;
307
+ break;
308
+ case "wechat":
309
+ case "xhs":
310
+ endpoint = `${API_BASE}/api/parse-meta`;
311
+ params = `url=${encodeURIComponent(url)}&_platform=${platform}`;
312
+ break;
313
+ case "youtube":
314
+ endpoint = `${API_BASE}/api/jina-extract`;
315
+ params = `url=${encodeURIComponent(url)}&_platform=youtube`;
316
+ break;
317
+ default:
318
+ endpoint = `${API_BASE}/api/jina-extract`;
319
+ params = `url=${encodeURIComponent(url)}`;
320
+ break;
321
+ }
322
+ const resp = await fetch(`${endpoint}?${params}`, {
323
+ headers: {
324
+ ...API_KEY ? { "x-api-key": API_KEY } : {},
325
+ "Accept": "application/json"
326
+ },
327
+ signal: AbortSignal.timeout(3e4)
328
+ });
329
+ if (!resp.ok) {
330
+ return { error: `API returned ${resp.status}` };
331
+ }
332
+ const data = await resp.json();
333
+ if (platform === "x") {
334
+ const text = data.text || data.article_text || "";
335
+ const quoteText = data.quote ? `
336
+
337
+ [Quote from @${data.quote.handle}]: ${data.quote.text}` : "";
499
338
  return {
500
- title: data.title,
501
- author: data.author,
502
- content: data.content || data.extracted_content || data.text || data.transcript || null,
339
+ title: data.text?.slice(0, 100) || "Tweet",
340
+ author: data.author ? `@${data.handle || data.author}` : void 0,
341
+ content: text + quoteText
503
342
  };
343
+ }
344
+ if (platform === "spotify") {
345
+ return {
346
+ title: data.title,
347
+ author: data.author,
348
+ content: data.extracted_content || data.description || null
349
+ };
350
+ }
351
+ if (platform === "wechat") {
352
+ return {
353
+ title: data.title,
354
+ author: data.author,
355
+ content: data.extracted_content || data.content || null
356
+ };
357
+ }
358
+ return {
359
+ title: data.title,
360
+ author: data.author,
361
+ content: data.content || data.extracted_content || data.text || data.transcript || null
362
+ };
363
+ } catch (err) {
364
+ return { error: err.message || "Fetch failed" };
504
365
  }
505
- catch (err) {
506
- return { error: err.message || 'Fetch failed' };
507
- }
508
- }
509
- async function handleFetchContent(args) {
366
+ }
367
+ async function handleFetchContent(args) {
510
368
  const { url } = args;
511
369
  const platform = detectPlatform(url);
512
- // Step 1: Check if we already have content in Supabase
513
- const { data: existing } = await supabase
514
- .from('bookmarks')
515
- .select('*')
516
- .eq('url', url)
517
- .limit(1)
518
- .maybeSingle();
370
+ const { data: existing } = await supabase.from("bookmarks").select("*").eq("url", url).limit(1).maybeSingle();
519
371
  if (existing) {
520
- const m = existing.content_metadata || {};
521
- if (m.extracted_content && m.extracted_content.length > 50) {
522
- return textResult(JSON.stringify({
523
- source: 'cache',
524
- url,
525
- platform,
526
- title: existing.title,
527
- author: m.author,
528
- content: m.extracted_content,
529
- aiPositioning: m.ai_positioning,
530
- aiTakeaway: m.ai_takeaway,
531
- tags: m.tags,
532
- }, null, 2));
533
- }
534
- }
535
- // Step 2: Fetch fresh content via Vercel API
536
- const result = await fetchViaAPI(url, platform);
537
- if (result.error) {
372
+ const m = existing.content_metadata || {};
373
+ if (m.extracted_content && m.extracted_content.length > 50) {
538
374
  return textResult(JSON.stringify({
539
- source: 'error',
540
- url,
541
- platform,
542
- error: result.error,
543
- hint: platform === 'x' ? 'X.com content is fetched via Vercel Edge proxy to bypass GFW' : undefined,
375
+ source: "cache",
376
+ url,
377
+ platform,
378
+ title: existing.title,
379
+ author: m.author,
380
+ content: m.extracted_content,
381
+ aiPositioning: m.ai_positioning,
382
+ aiTakeaway: m.ai_takeaway,
383
+ tags: m.tags
544
384
  }, null, 2));
385
+ }
545
386
  }
546
- return textResult(JSON.stringify({
547
- source: 'live',
387
+ const result = await fetchViaAPI(url, platform);
388
+ if (result.error) {
389
+ return textResult(JSON.stringify({
390
+ source: "error",
548
391
  url,
549
392
  platform,
550
- title: result.title,
551
- author: result.author,
552
- content: result.content,
393
+ error: result.error,
394
+ hint: platform === "x" ? "X.com content is fetched via Vercel Edge proxy to bypass GFW" : void 0
395
+ }, null, 2));
396
+ }
397
+ return textResult(JSON.stringify({
398
+ source: "live",
399
+ url,
400
+ platform,
401
+ title: result.title,
402
+ author: result.author,
403
+ content: result.content
553
404
  }, null, 2));
554
- }
555
- async function handleListSparks(args) {
556
- const { data, error } = await supabase
557
- .from('bookmarks')
558
- .select('*')
559
- .eq('status', 'read')
560
- .order('created_at', { ascending: false })
561
- .limit(args.limit || 20);
562
- if (error)
563
- return textResult(`Error: ${error.message}`);
405
+ }
406
+ async function handleListSparks(args) {
407
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("status", "read").order("created_at", { ascending: false }).limit(args.limit || 20);
408
+ if (error) return textResult(`Error: ${error.message}`);
564
409
  const results = (data || []).map((row) => {
565
- const s = metaSummary(row);
566
- const m = row.content_metadata || {};
567
- return {
568
- ...s,
569
- sparkInsight: m.spark_insight || null,
570
- sparkExpiresAt: m.spark_expires_at || null,
571
- };
410
+ const s = metaSummary(row);
411
+ const m = row.content_metadata || {};
412
+ return {
413
+ ...s,
414
+ sparkInsight: m.spark_insight || null,
415
+ sparkExpiresAt: m.spark_expires_at || null
416
+ };
572
417
  });
573
418
  return textResult(JSON.stringify(results, null, 2));
574
- }
575
- async function handleSearchSparks(args) {
419
+ }
420
+ async function handleSearchSparks(args) {
576
421
  const { query, limit } = args;
577
- const { data, error } = await supabase
578
- .from('bookmarks')
579
- .select('*')
580
- .eq('status', 'read')
581
- .order('created_at', { ascending: false })
582
- .limit(50);
583
- if (error)
584
- return textResult(`Error: ${error.message}`);
585
- const results = (data || [])
586
- .filter((row) => {
587
- const m = row.content_metadata || {};
588
- const searchable = [
589
- row.title || '',
590
- ...(m.tags || []),
591
- ...(m.ai_takeaway || []),
592
- m.ai_positioning || '',
593
- m.spark_insight || '',
594
- ].join(' ').toLowerCase();
595
- return searchable.includes(query.toLowerCase());
596
- })
597
- .slice(0, limit || 10)
598
- .map((row) => {
599
- const s = metaSummary(row);
600
- const m = row.content_metadata || {};
601
- return {
602
- ...s,
603
- sparkInsight: m.spark_insight || null,
604
- sparkExpiresAt: m.spark_expires_at || null,
605
- };
422
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("status", "read").order("created_at", { ascending: false }).limit(50);
423
+ if (error) return textResult(`Error: ${error.message}`);
424
+ const results = (data || []).filter((row) => {
425
+ const m = row.content_metadata || {};
426
+ const searchable = [
427
+ row.title || "",
428
+ ...m.tags || [],
429
+ ...m.ai_takeaway || [],
430
+ m.ai_positioning || "",
431
+ m.spark_insight || ""
432
+ ].join(" ").toLowerCase();
433
+ return searchable.includes(query.toLowerCase());
434
+ }).slice(0, limit || 10).map((row) => {
435
+ const s = metaSummary(row);
436
+ const m = row.content_metadata || {};
437
+ return {
438
+ ...s,
439
+ sparkInsight: m.spark_insight || null,
440
+ sparkExpiresAt: m.spark_expires_at || null
441
+ };
606
442
  });
607
443
  return textResult(JSON.stringify(results, null, 2));
608
- }
609
- async function handleListFlame(args) {
610
- const { data, error } = await supabase
611
- .from('bookmarks')
612
- .select('*')
613
- .eq('status', 'active')
614
- .order('created_at', { ascending: false })
615
- .limit(args.limit || 20);
616
- if (error)
617
- return textResult(`Error: ${error.message}`);
618
- // Filter out already expired ones (should be ash but not yet processed)
619
- const now = new Date();
620
- const results = (data || [])
621
- .filter((row) => {
622
- if (!row.countdown_expires_at)
623
- return true;
624
- return new Date(row.countdown_expires_at).getTime() > now.getTime();
625
- })
626
- .map(flameSummary);
444
+ }
445
+ async function handleListFlame(args) {
446
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("status", "active").order("created_at", { ascending: false }).limit(args.limit || 20);
447
+ if (error) return textResult(`Error: ${error.message}`);
448
+ const now = /* @__PURE__ */ new Date();
449
+ const results = (data || []).filter((row) => {
450
+ if (!row.countdown_expires_at) return true;
451
+ return new Date(row.countdown_expires_at).getTime() > now.getTime();
452
+ }).map(flameSummary);
627
453
  return textResult(JSON.stringify(results, null, 2));
628
- }
629
- async function handleGetFlameDetail(args) {
630
- const { data, error } = await supabase
631
- .from('bookmarks')
632
- .select('*')
633
- .eq('id', args.id)
634
- .eq('status', 'active')
635
- .single();
454
+ }
455
+ async function handleGetFlameDetail(args) {
456
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("id", args.id).eq("status", "active").single();
636
457
  if (error) {
637
- return textResult(error.code === 'PGRST116'
638
- ? `No active Flame bookmark found with id "${args.id}". It may have already burned to Ash or been moved to Spark/Vault.`
639
- : `Error: ${error.message}`);
458
+ return textResult(
459
+ error.code === "PGRST116" ? `No active Flame bookmark found with id "${args.id}". It may have already burned to Ash or been moved to Spark/Vault.` : `Error: ${error.message}`
460
+ );
640
461
  }
641
- // Return full detail including extracted content
642
462
  const m = data.content_metadata || {};
643
463
  const expiresAt = data.countdown_expires_at ? new Date(data.countdown_expires_at) : null;
644
- const now = new Date();
464
+ const now = /* @__PURE__ */ new Date();
645
465
  const remainingMs = expiresAt ? expiresAt.getTime() - now.getTime() : 0;
646
- const remainingHours = Math.max(0, Math.round(remainingMs / 3600000 * 10) / 10);
466
+ const remainingHours = Math.max(0, Math.round(remainingMs / 36e5 * 10) / 10);
647
467
  const result = {
648
- ...flameSummary(data),
649
- extractedContent: m.extracted_content || null,
650
- externalURL: m.external_url || null,
651
- thumbnail: m.thumbnail || null,
652
- aiFocus: m.ai_focus || null,
653
- aiUse: m.ai_use || null,
654
- aiBuzz: m.ai_buzz || null,
468
+ ...flameSummary(data),
469
+ extractedContent: m.extracted_content || null,
470
+ externalURL: m.external_url || null,
471
+ thumbnail: m.thumbnail || null,
472
+ aiFocus: m.ai_focus || null,
473
+ aiUse: m.ai_use || null,
474
+ aiBuzz: m.ai_buzz || null
655
475
  };
656
476
  return textResult(JSON.stringify(result, null, 2));
657
- }
658
- async function handleListVault(args) {
659
- let query = supabase
660
- .from('bookmarks')
661
- .select('*')
662
- .eq('status', 'absorbed')
663
- .order('created_at', { ascending: false })
664
- .limit(args.limit || 20);
477
+ }
478
+ async function handleListVault(args) {
479
+ let query = supabase.from("bookmarks").select("*").eq("status", "absorbed").order("created_at", { ascending: false }).limit(args.limit || 20);
665
480
  const { data, error } = await query;
666
- if (error)
667
- return textResult(`Error: ${error.message}`);
481
+ if (error) return textResult(`Error: ${error.message}`);
668
482
  let results = (data || []).map(metaSummary);
669
- // Filter by category if provided
670
483
  if (args.category) {
671
- results = results.filter((r) => r.vaultCategory?.toLowerCase() === args.category.toLowerCase());
484
+ results = results.filter(
485
+ (r) => r.vaultCategory?.toLowerCase() === args.category.toLowerCase()
486
+ );
672
487
  }
673
488
  return textResult(JSON.stringify(results, null, 2));
674
- }
675
- // ---------------------------------------------------------------------------
676
- // Layer 1: Status flow handlers (决策层)
677
- // ---------------------------------------------------------------------------
678
- async function handleMoveFlameToSpark(args) {
679
- const { data, error } = await verifyBookmark(args.id, 'active');
680
- if (error)
681
- return textResult(`Error: ${error}`);
682
- const sparkExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
489
+ }
490
+ async function handleMoveFlameToSpark(args) {
491
+ const { data, error } = await verifyBookmark(args.id, "active");
492
+ if (error) return textResult(`Error: ${error}`);
493
+ const sparkExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString();
683
494
  const metaFields = { spark_expires_at: sparkExpiresAt };
684
- if (args.spark_insight)
685
- metaFields.spark_insight = args.spark_insight;
495
+ if (args.spark_insight) metaFields.spark_insight = args.spark_insight;
686
496
  const { error: mergeErr } = await mergeContentMetadata(args.id, metaFields, {
687
- status: 'read',
688
- read_at: new Date().toISOString(),
497
+ status: "read",
498
+ read_at: (/* @__PURE__ */ new Date()).toISOString()
689
499
  });
690
- if (mergeErr)
691
- return textResult(`Error: ${mergeErr}`);
500
+ if (mergeErr) return textResult(`Error: ${mergeErr}`);
692
501
  return textResult(JSON.stringify({
693
- success: true,
694
- id: args.id,
695
- title: data.title,
696
- action: 'flame spark',
697
- sparkExpiresAt,
502
+ success: true,
503
+ id: args.id,
504
+ title: data.title,
505
+ action: "flame \u2192 spark",
506
+ sparkExpiresAt
698
507
  }, null, 2));
699
- }
700
- async function handleMoveFlameToAsh(args) {
701
- const { data, error } = await verifyBookmark(args.id, 'active');
702
- if (error)
703
- return textResult(`Error: ${error}`);
704
- const { error: updateErr } = await supabase
705
- .from('bookmarks')
706
- .update({ status: 'ash' })
707
- .eq('id', args.id);
708
- if (updateErr)
709
- return textResult(`Error: ${updateErr.message}`);
508
+ }
509
+ async function handleMoveFlameToAsh(args) {
510
+ const { data, error } = await verifyBookmark(args.id, "active");
511
+ if (error) return textResult(`Error: ${error}`);
512
+ const { error: updateErr } = await supabase.from("bookmarks").update({ status: "ash" }).eq("id", args.id);
513
+ if (updateErr) return textResult(`Error: ${updateErr.message}`);
710
514
  return textResult(JSON.stringify({
711
- success: true,
712
- id: args.id,
713
- title: data.title,
714
- action: 'flame ash',
715
- reason: args.reason || null,
515
+ success: true,
516
+ id: args.id,
517
+ title: data.title,
518
+ action: "flame \u2192 ash",
519
+ reason: args.reason || null
716
520
  }, null, 2));
717
- }
718
- async function handleMoveSparkToVault(args) {
719
- const { data, error } = await verifyBookmark(args.id, 'read');
720
- if (error)
721
- return textResult(`Error: ${error}`);
722
- const metaFields = { vaulted_at: new Date().toISOString() };
723
- if (args.vault_category)
724
- metaFields.vault_category = args.vault_category;
521
+ }
522
+ async function handleMoveSparkToVault(args) {
523
+ const { data, error } = await verifyBookmark(args.id, "read");
524
+ if (error) return textResult(`Error: ${error}`);
525
+ const metaFields = { vaulted_at: (/* @__PURE__ */ new Date()).toISOString() };
526
+ if (args.vault_category) metaFields.vault_category = args.vault_category;
725
527
  const { error: mergeErr } = await mergeContentMetadata(args.id, metaFields, {
726
- status: 'absorbed',
528
+ status: "absorbed"
727
529
  });
728
- if (mergeErr)
729
- return textResult(`Error: ${mergeErr}`);
530
+ if (mergeErr) return textResult(`Error: ${mergeErr}`);
730
531
  return textResult(JSON.stringify({
731
- success: true,
732
- id: args.id,
733
- title: data.title,
734
- action: 'spark vault',
735
- vaultCategory: args.vault_category || null,
532
+ success: true,
533
+ id: args.id,
534
+ title: data.title,
535
+ action: "spark \u2192 vault",
536
+ vaultCategory: args.vault_category || null
736
537
  }, null, 2));
737
- }
738
- async function handleMoveSparkToAsh(args) {
739
- const { data, error } = await verifyBookmark(args.id, 'read');
740
- if (error)
741
- return textResult(`Error: ${error}`);
742
- const { error: updateErr } = await supabase
743
- .from('bookmarks')
744
- .update({ status: 'ash' })
745
- .eq('id', args.id);
746
- if (updateErr)
747
- return textResult(`Error: ${updateErr.message}`);
538
+ }
539
+ async function handleMoveSparkToAsh(args) {
540
+ const { data, error } = await verifyBookmark(args.id, "read");
541
+ if (error) return textResult(`Error: ${error}`);
542
+ const { error: updateErr } = await supabase.from("bookmarks").update({ status: "ash" }).eq("id", args.id);
543
+ if (updateErr) return textResult(`Error: ${updateErr.message}`);
748
544
  return textResult(JSON.stringify({
749
- success: true,
750
- id: args.id,
751
- title: data.title,
752
- action: 'spark ash',
545
+ success: true,
546
+ id: args.id,
547
+ title: data.title,
548
+ action: "spark \u2192 ash"
753
549
  }, null, 2));
754
- }
755
- async function handleBatchTriageFlame(args) {
550
+ }
551
+ async function handleBatchTriageFlame(args) {
756
552
  const results = [];
757
553
  for (const decision of args.decisions) {
758
- if (decision.action === 'spark') {
759
- const res = await handleMoveFlameToSpark({ id: decision.id, spark_insight: decision.spark_insight });
760
- const parsed = JSON.parse(res.content[0].text);
761
- results.push({ id: decision.id, action: 'flame spark', success: !!parsed.success, error: parsed.success ? undefined : res.content[0].text, title: parsed.title });
762
- }
763
- else {
764
- const res = await handleMoveFlameToAsh({ id: decision.id });
765
- const parsed = JSON.parse(res.content[0].text);
766
- results.push({ id: decision.id, action: 'flame → ash', success: !!parsed.success, error: parsed.success ? undefined : res.content[0].text, title: parsed.title });
767
- }
554
+ if (decision.action === "spark") {
555
+ const res = await handleMoveFlameToSpark({ id: decision.id, spark_insight: decision.spark_insight });
556
+ const parsed = JSON.parse(res.content[0].text);
557
+ results.push({ id: decision.id, action: "flame \u2192 spark", success: !!parsed.success, error: parsed.success ? void 0 : res.content[0].text, title: parsed.title });
558
+ } else {
559
+ const res = await handleMoveFlameToAsh({ id: decision.id });
560
+ const parsed = JSON.parse(res.content[0].text);
561
+ results.push({ id: decision.id, action: "flame \u2192 ash", success: !!parsed.success, error: parsed.success ? void 0 : res.content[0].text, title: parsed.title });
562
+ }
768
563
  }
769
- const succeeded = results.filter(r => r.success).length;
770
- const failed = results.filter(r => !r.success).length;
564
+ const succeeded = results.filter((r) => r.success).length;
565
+ const failed = results.filter((r) => !r.success).length;
771
566
  return textResult(JSON.stringify({
772
- summary: `${succeeded} succeeded, ${failed} failed (of ${results.length} total)`,
773
- results,
567
+ summary: `${succeeded} succeeded, ${failed} failed (of ${results.length} total)`,
568
+ results
774
569
  }, null, 2));
775
- }
776
- // ---------------------------------------------------------------------------
777
- // Layer 3: AI analysis writeback handler (分析层)
778
- // ---------------------------------------------------------------------------
779
- async function handleWriteBookmarkAnalysis(args) {
570
+ }
571
+ async function handleWriteBookmarkAnalysis(args) {
780
572
  const { data, error } = await verifyBookmark(args.id);
781
- if (error)
782
- return textResult(`Error: ${error}`);
573
+ if (error) return textResult(`Error: ${error}`);
783
574
  const { error: mergeErr } = await mergeContentMetadata(args.id, args.analysis);
784
- if (mergeErr)
785
- return textResult(`Error: ${mergeErr}`);
786
- const fieldsWritten = Object.keys(args.analysis).filter(k => args.analysis[k] !== undefined);
575
+ if (mergeErr) return textResult(`Error: ${mergeErr}`);
576
+ const fieldsWritten = Object.keys(args.analysis).filter((k) => args.analysis[k] !== void 0);
787
577
  return textResult(JSON.stringify({
788
- success: true,
789
- id: args.id,
790
- title: data.title,
791
- fieldsWritten,
578
+ success: true,
579
+ id: args.id,
580
+ title: data.title,
581
+ fieldsWritten
792
582
  }, null, 2));
793
- }
794
- // ---------------------------------------------------------------------------
795
- // Layer 2: Collection handlers (组合层)
796
- // ---------------------------------------------------------------------------
797
- async function handleCreateCollection(args) {
798
- // Get user ID from an existing bookmark (RLS ensures we only see our own)
799
- const { data: sample } = await supabase
800
- .from('bookmarks')
801
- .select('user_id')
802
- .limit(1)
803
- .single();
804
- if (!sample)
805
- return textResult('Error: No bookmarks found — cannot determine user ID');
583
+ }
584
+ async function handleCreateCollection(args) {
585
+ const { data: sample } = await supabase.from("bookmarks").select("user_id").limit(1).single();
586
+ if (!sample) return textResult("Error: No bookmarks found \u2014 cannot determine user ID");
806
587
  const bookmarkIds = args.bookmark_ids || [];
807
- // Verify bookmark_ids exist if provided
808
588
  if (bookmarkIds.length > 0) {
809
- const { data: existing } = await supabase
810
- .from('bookmarks')
811
- .select('id')
812
- .in('id', bookmarkIds);
813
- const existingIds = new Set((existing || []).map((b) => b.id));
814
- const missing = bookmarkIds.filter(id => !existingIds.has(id));
815
- if (missing.length > 0) {
816
- return textResult(`Error: Bookmark IDs not found: ${missing.join(', ')}`);
817
- }
589
+ const { data: existing } = await supabase.from("bookmarks").select("id").in("id", bookmarkIds);
590
+ const existingIds = new Set((existing || []).map((b) => b.id));
591
+ const missing = bookmarkIds.filter((id) => !existingIds.has(id));
592
+ if (missing.length > 0) {
593
+ return textResult(`Error: Bookmark IDs not found: ${missing.join(", ")}`);
594
+ }
818
595
  }
819
- const { data, error } = await supabase
820
- .from('collections')
821
- .insert({
822
- user_id: sample.user_id,
823
- name: args.name,
824
- bookmark_ids: bookmarkIds,
825
- is_overview_stale: true,
826
- })
827
- .select()
828
- .single();
829
- if (error)
830
- return textResult(`Error: ${error.message}`);
596
+ const { data, error } = await supabase.from("collections").insert({
597
+ user_id: sample.user_id,
598
+ name: args.name,
599
+ bookmark_ids: bookmarkIds,
600
+ is_overview_stale: true
601
+ }).select().single();
602
+ if (error) return textResult(`Error: ${error.message}`);
831
603
  return textResult(JSON.stringify({
832
- success: true,
833
- id: data.id,
834
- name: data.name,
835
- articleCount: bookmarkIds.length,
604
+ success: true,
605
+ id: data.id,
606
+ name: data.name,
607
+ articleCount: bookmarkIds.length
836
608
  }, null, 2));
837
- }
838
- async function handleAddToCollection(args) {
839
- const { data: collection, error } = await supabase
840
- .from('collections')
841
- .select('*')
842
- .eq('id', args.collection_id)
843
- .single();
609
+ }
610
+ async function handleAddToCollection(args) {
611
+ const { data: collection, error } = await supabase.from("collections").select("*").eq("id", args.collection_id).single();
844
612
  if (error) {
845
- return textResult(error.code === 'PGRST116' ? 'Error: Collection not found' : `Error: ${error.message}`);
613
+ return textResult(error.code === "PGRST116" ? "Error: Collection not found" : `Error: ${error.message}`);
846
614
  }
847
- // Verify bookmark_ids exist
848
- const { data: existing } = await supabase
849
- .from('bookmarks')
850
- .select('id')
851
- .in('id', args.bookmark_ids);
615
+ const { data: existing } = await supabase.from("bookmarks").select("id").in("id", args.bookmark_ids);
852
616
  const existingIds = new Set((existing || []).map((b) => b.id));
853
- const missing = args.bookmark_ids.filter(id => !existingIds.has(id));
617
+ const missing = args.bookmark_ids.filter((id) => !existingIds.has(id));
854
618
  if (missing.length > 0) {
855
- return textResult(`Error: Bookmark IDs not found: ${missing.join(', ')}`);
619
+ return textResult(`Error: Bookmark IDs not found: ${missing.join(", ")}`);
856
620
  }
857
- // Union with existing (deduplicate)
858
621
  const currentIds = new Set(collection.bookmark_ids || []);
859
- const newIds = args.bookmark_ids.filter(id => !currentIds.has(id));
860
- const merged = [...(collection.bookmark_ids || []), ...newIds];
861
- const { error: updateErr } = await supabase
862
- .from('collections')
863
- .update({ bookmark_ids: merged, is_overview_stale: true })
864
- .eq('id', args.collection_id);
865
- if (updateErr)
866
- return textResult(`Error: ${updateErr.message}`);
622
+ const newIds = args.bookmark_ids.filter((id) => !currentIds.has(id));
623
+ const merged = [...collection.bookmark_ids || [], ...newIds];
624
+ const { error: updateErr } = await supabase.from("collections").update({ bookmark_ids: merged, is_overview_stale: true }).eq("id", args.collection_id);
625
+ if (updateErr) return textResult(`Error: ${updateErr.message}`);
867
626
  return textResult(JSON.stringify({
868
- success: true,
869
- collectionId: args.collection_id,
870
- name: collection.name,
871
- added: newIds.length,
872
- alreadyPresent: args.bookmark_ids.length - newIds.length,
873
- totalArticles: merged.length,
627
+ success: true,
628
+ collectionId: args.collection_id,
629
+ name: collection.name,
630
+ added: newIds.length,
631
+ alreadyPresent: args.bookmark_ids.length - newIds.length,
632
+ totalArticles: merged.length
874
633
  }, null, 2));
875
- }
876
- async function handleRemoveFromCollection(args) {
877
- const { data: collection, error } = await supabase
878
- .from('collections')
879
- .select('*')
880
- .eq('id', args.collection_id)
881
- .single();
634
+ }
635
+ async function handleRemoveFromCollection(args) {
636
+ const { data: collection, error } = await supabase.from("collections").select("*").eq("id", args.collection_id).single();
882
637
  if (error) {
883
- return textResult(error.code === 'PGRST116' ? 'Error: Collection not found' : `Error: ${error.message}`);
638
+ return textResult(error.code === "PGRST116" ? "Error: Collection not found" : `Error: ${error.message}`);
884
639
  }
885
640
  const removeSet = new Set(args.bookmark_ids);
886
641
  const filtered = (collection.bookmark_ids || []).filter((id) => !removeSet.has(id));
887
642
  const removed = (collection.bookmark_ids || []).length - filtered.length;
888
- const { error: updateErr } = await supabase
889
- .from('collections')
890
- .update({ bookmark_ids: filtered, is_overview_stale: true })
891
- .eq('id', args.collection_id);
892
- if (updateErr)
893
- return textResult(`Error: ${updateErr.message}`);
643
+ const { error: updateErr } = await supabase.from("collections").update({ bookmark_ids: filtered, is_overview_stale: true }).eq("id", args.collection_id);
644
+ if (updateErr) return textResult(`Error: ${updateErr.message}`);
894
645
  return textResult(JSON.stringify({
895
- success: true,
896
- collectionId: args.collection_id,
897
- name: collection.name,
898
- removed,
899
- totalArticles: filtered.length,
646
+ success: true,
647
+ collectionId: args.collection_id,
648
+ name: collection.name,
649
+ removed,
650
+ totalArticles: filtered.length
900
651
  }, null, 2));
901
- }
902
- async function handleUpdateCollectionOverview(args) {
903
- const { data: collection, error } = await supabase
904
- .from('collections')
905
- .select('id, name')
906
- .eq('id', args.collection_id)
907
- .single();
652
+ }
653
+ async function handleUpdateCollectionOverview(args) {
654
+ const { data: collection, error } = await supabase.from("collections").select("id, name").eq("id", args.collection_id).single();
908
655
  if (error) {
909
- return textResult(error.code === 'PGRST116' ? 'Error: Collection not found' : `Error: ${error.message}`);
656
+ return textResult(error.code === "PGRST116" ? "Error: Collection not found" : `Error: ${error.message}`);
910
657
  }
911
- const { error: updateErr } = await supabase
912
- .from('collections')
913
- .update({ ai_overview: args.overview, is_overview_stale: false })
914
- .eq('id', args.collection_id);
915
- if (updateErr)
916
- return textResult(`Error: ${updateErr.message}`);
658
+ const { error: updateErr } = await supabase.from("collections").update({ ai_overview: args.overview, is_overview_stale: false }).eq("id", args.collection_id);
659
+ if (updateErr) return textResult(`Error: ${updateErr.message}`);
917
660
  return textResult(JSON.stringify({
918
- success: true,
919
- collectionId: args.collection_id,
920
- name: collection.name,
921
- overviewTheme: args.overview.theme,
661
+ success: true,
662
+ collectionId: args.collection_id,
663
+ name: collection.name,
664
+ overviewTheme: args.overview.theme
922
665
  }, null, 2));
923
- }
924
- // ---------------------------------------------------------------------------
925
- // Register tools
926
- // ---------------------------------------------------------------------------
927
- // @ts-expect-error — MCP SDK 1.27 TS2589: type instantiation too deep with multiple .tool() calls
928
- server.tool('search_vault', 'Search your Burn Vault for bookmarks by keyword (searches title, tags, AI takeaway)', { query: zod_1.z.string().describe('Search keyword'), limit: zod_1.z.number().optional().describe('Max results (default 10)') }, rateLimited(handleSearchVault));
929
- server.tool('list_vault', 'List bookmarks in your Vault, optionally filtered by category', { limit: zod_1.z.number().optional().describe('Max results (default 20)'), category: zod_1.z.string().optional().describe('Filter by vault category') }, rateLimited(handleListVault));
930
- server.tool('list_sparks', 'List your Sparks (bookmarks you have read, with 30-day lifespan). Includes spark insight and expiry date.', { limit: zod_1.z.number().optional().describe('Max results (default 20)') }, rateLimited(handleListSparks));
931
- server.tool('search_sparks', 'Search your Sparks by keyword (searches title, tags, AI takeaway, spark insight)', { query: zod_1.z.string().describe('Search keyword'), limit: zod_1.z.number().optional().describe('Max results (default 10)') }, rateLimited(handleSearchSparks));
932
- server.tool('get_bookmark', 'Get full details of a single bookmark including AI analysis and extracted content', { id: zod_1.z.string().describe('Bookmark UUID') }, rateLimited(handleGetBookmark));
933
- server.tool('get_article_content', 'Get full article content and AI analysis for a bookmark by ID (same as get_bookmark)', { id: zod_1.z.string().describe('Bookmark UUID') }, rateLimited(handleGetArticleContent));
934
- server.tool('fetch_content', 'Fetch article/tweet content from a URL. Works with X.com (bypasses GFW via proxy), Reddit, YouTube, Bilibili, WeChat, and any web page. First checks Supabase cache, then fetches live.', { url: zod_1.z.string().describe('The URL to fetch content from') }, rateLimited(handleFetchContent));
935
- server.tool('list_categories', 'List all Vault categories with article counts', {}, rateLimited(handleListCategories));
936
- server.tool('list_flame', 'List bookmarks in your Flame inbox (24h countdown). Shows AI triage info (strategy, relevance, novelty, hook) and time remaining. Use this to see what needs attention before it burns to Ash.', { limit: zod_1.z.number().optional().describe('Max results (default 20)') }, rateLimited(handleListFlame));
937
- server.tool('get_flame_detail', 'Get full details of a Flame bookmark including extracted article content, AI analysis, and reading guidance. Use this to deep-read a bookmark before deciding its fate.', { id: zod_1.z.string().describe('Bookmark UUID') }, rateLimited(handleGetFlameDetail));
938
- server.tool('get_collections', 'List all your Collections with article counts and AI overview themes', {}, rateLimited(handleGetCollections));
939
- server.tool('get_collection_overview', 'Get a Collection by name with its AI overview and linked bookmarks metadata', { name: zod_1.z.string().describe('Collection name') }, rateLimited(handleGetCollectionOverview));
940
- // ---------------------------------------------------------------------------
941
- // Layer 1: Status flow tools (决策层)
942
- // ---------------------------------------------------------------------------
943
- server.tool('move_flame_to_spark', 'Move a Flame bookmark to Spark (mark as worth reading). Sets 30-day Spark lifespan.', {
944
- id: zod_1.z.string().describe('Bookmark UUID'),
945
- spark_insight: zod_1.z.string().max(500).optional().describe('One-line insight about why this is worth reading'),
946
- }, rateLimited(handleMoveFlameToSpark));
947
- server.tool('move_flame_to_ash', 'Burn a Flame bookmark to Ash (not worth keeping).', {
948
- id: zod_1.z.string().describe('Bookmark UUID'),
949
- reason: zod_1.z.string().max(200).optional().describe('Why this was burned'),
950
- }, rateLimited(handleMoveFlameToAsh));
951
- server.tool('move_spark_to_vault', 'Promote a Spark bookmark to permanent Vault storage.', {
952
- id: zod_1.z.string().describe('Bookmark UUID'),
953
- vault_category: zod_1.z.string().max(100).optional().describe('Category to file under in the Vault'),
954
- }, rateLimited(handleMoveSparkToVault));
955
- server.tool('move_spark_to_ash', 'Burn a Spark bookmark to Ash (not valuable enough to vault).', {
956
- id: zod_1.z.string().describe('Bookmark UUID'),
957
- }, rateLimited(handleMoveSparkToAsh));
958
- // @ts-expect-error — MCP SDK TS2589
959
- server.tool('batch_triage_flame', 'Triage multiple Flame bookmarks at once. Each decision moves a bookmark to Spark or Ash.', {
960
- decisions: zod_1.z.array(zod_1.z.object({
961
- id: zod_1.z.string().describe('Bookmark UUID'),
962
- action: zod_1.z.enum(['spark', 'ash']).describe('spark = keep, ash = burn'),
963
- spark_insight: zod_1.z.string().max(500).optional().describe('Insight (only for spark action)'),
964
- })).min(1).max(20).describe('Array of triage decisions'),
965
- }, rateLimited(handleBatchTriageFlame));
966
- // ---------------------------------------------------------------------------
967
- // Layer 3: AI analysis writeback tools (分析层)
968
- // ---------------------------------------------------------------------------
969
- // @ts-expect-error — MCP SDK TS2589
970
- server.tool('write_bookmark_analysis', 'Write AI analysis results into a bookmark. Agent analyzes content with its own LLM, then writes structured results back to Burn. Only provided fields are merged — existing data is preserved.', {
971
- id: zod_1.z.string().describe('Bookmark UUID'),
972
- analysis: zod_1.z.object({
973
- ai_summary: zod_1.z.string().max(200).optional().describe('One-line summary'),
974
- ai_strategy: zod_1.z.enum(['deep_read', 'skim', 'skip_read', 'reference']).optional().describe('Reading strategy'),
975
- ai_strategy_reason: zod_1.z.string().max(200).optional().describe('Why this strategy'),
976
- ai_minutes: zod_1.z.number().int().min(1).max(999).optional().describe('Estimated reading minutes'),
977
- ai_takeaway: zod_1.z.array(zod_1.z.string().max(200)).max(5).optional().describe('Key takeaways'),
978
- ai_relevance: zod_1.z.number().int().min(0).max(100).optional().describe('Relevance score 0-100'),
979
- ai_novelty: zod_1.z.number().int().min(0).max(100).optional().describe('Novelty score 0-100'),
980
- tags: zod_1.z.array(zod_1.z.string().max(50)).max(10).optional().describe('Topic tags'),
981
- }).describe('Analysis fields to write'),
982
- }, rateLimited(handleWriteBookmarkAnalysis));
983
- // ---------------------------------------------------------------------------
984
- // Layer 2: Collection tools (组合层)
985
- // ---------------------------------------------------------------------------
986
- // @ts-expect-error — MCP SDK TS2589
987
- server.tool('create_collection', 'Create a new Collection to group related bookmarks together.', {
988
- name: zod_1.z.string().min(1).max(200).describe('Collection name'),
989
- bookmark_ids: zod_1.z.array(zod_1.z.string()).optional().describe('Initial bookmark UUIDs to include'),
990
- }, rateLimited(handleCreateCollection));
991
- // @ts-expect-error — MCP SDK TS2589
992
- server.tool('add_to_collection', 'Add bookmarks to an existing Collection. Duplicates are silently ignored.', {
993
- collection_id: zod_1.z.string().describe('Collection UUID'),
994
- bookmark_ids: zod_1.z.array(zod_1.z.string()).min(1).max(50).describe('Bookmark UUIDs to add'),
995
- }, rateLimited(handleAddToCollection));
996
- server.tool('remove_from_collection', 'Remove bookmarks from a Collection.', {
997
- collection_id: zod_1.z.string().describe('Collection UUID'),
998
- bookmark_ids: zod_1.z.array(zod_1.z.string()).min(1).describe('Bookmark UUIDs to remove'),
999
- }, rateLimited(handleRemoveFromCollection));
1000
- // @ts-expect-error — MCP SDK TS2589
1001
- server.tool('update_collection_overview', 'Write an AI-generated overview for a Collection (theme, synthesis, patterns, gaps).', {
1002
- collection_id: zod_1.z.string().describe('Collection UUID'),
1003
- overview: zod_1.z.object({
1004
- theme: zod_1.z.string().describe('Overarching theme'),
1005
- synthesis: zod_1.z.string().optional().describe('Cross-bookmark synthesis'),
1006
- patterns: zod_1.z.array(zod_1.z.string()).optional().describe('Patterns identified'),
1007
- gaps: zod_1.z.array(zod_1.z.string()).optional().describe('Knowledge gaps identified'),
1008
- }).describe('AI-generated overview'),
1009
- }, rateLimited(handleUpdateCollectionOverview));
1010
- // ---------------------------------------------------------------------------
1011
- // Resource: burn://vault/bookmarks
1012
- // ---------------------------------------------------------------------------
1013
- server.resource('vault-bookmarks', 'burn://vault/bookmarks', async (uri) => {
1014
- const { data, error } = await supabase
1015
- .from('bookmarks')
1016
- .select('*')
1017
- .eq('status', 'absorbed')
1018
- .order('created_at', { ascending: false });
1019
- if (error) {
1020
- return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: error.message }) }] };
666
+ }
667
+ function decodeXMLEntities(str) {
668
+ return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&apos;/g, "'").replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)));
669
+ }
670
+ function extractXMLValue(block, tag) {
671
+ const cdataRe = new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`, "i");
672
+ const cdata = block.match(cdataRe);
673
+ if (cdata) return cdata[1].trim();
674
+ if (tag === "link") {
675
+ const href = block.match(/<link[^>]+href=["']([^"']+)["'][^>]*(?:\/>|>)/i);
676
+ if (href) return href[1].trim();
1021
677
  }
1022
- return {
678
+ const normalRe = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i");
679
+ const normal = block.match(normalRe);
680
+ if (normal) return decodeXMLEntities(normal[1].trim());
681
+ return null;
682
+ }
683
+ function parseRSSFeed(xml) {
684
+ const items = [];
685
+ const itemRe = /<(?:item|entry)(?: [^>]*)?>([\s\S]*?)<\/(?:item|entry)>/gi;
686
+ let m;
687
+ while ((m = itemRe.exec(xml)) !== null) {
688
+ const block = m[1];
689
+ const title = extractXMLValue(block, "title") || "Untitled";
690
+ const rawUrl = extractXMLValue(block, "link") || extractXMLValue(block, "id") || "";
691
+ if (!rawUrl.startsWith("http")) continue;
692
+ const url = rawUrl.replace(/^https?:\/\/nitter\.[^/]+/, "https://x.com");
693
+ const pubStr = extractXMLValue(block, "pubDate") || extractXMLValue(block, "published") || extractXMLValue(block, "updated") || "";
694
+ let publishedAt = (/* @__PURE__ */ new Date()).toISOString();
695
+ try {
696
+ if (pubStr) publishedAt = new Date(pubStr).toISOString();
697
+ } catch {
698
+ }
699
+ const author = extractXMLValue(block, "author") || extractXMLValue(block, "dc:creator") || "";
700
+ items.push({ url, title, author, publishedAt });
701
+ }
702
+ return items;
703
+ }
704
+ async function fetchRSSFeed(feedUrl) {
705
+ const resp = await fetch(feedUrl, {
706
+ signal: AbortSignal.timeout(12e3),
707
+ headers: {
708
+ // Use browser UA — many RSS hosts (bearblog, Substack) block bot UAs
709
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
710
+ Accept: "application/rss+xml, application/atom+xml, text/xml, */*"
711
+ }
712
+ });
713
+ if (!resp.ok) throw new Error(`HTTP ${resp.status} from ${feedUrl}`);
714
+ return parseRSSFeed(await resp.text());
715
+ }
716
+ async function getSourceItems(source) {
717
+ const since = source.last_checked_at ? new Date(source.last_checked_at) : /* @__PURE__ */ new Date(0);
718
+ switch (source.source_type) {
719
+ case "x_user": {
720
+ throw new Error(`X/Twitter timeline scraping is unavailable \u2014 public nitter/RSS proxies are offline. To add @${source.handle} tweets, use fetch_content with individual tweet URLs.`);
721
+ }
722
+ case "rss": {
723
+ const items = await fetchRSSFeed(source.handle);
724
+ return items.filter((i) => new Date(i.publishedAt) > since);
725
+ }
726
+ case "youtube": {
727
+ const channelId = source.handle.match(/UC[A-Za-z0-9_-]{21}[AQgw]/)?.[0] || source.handle;
728
+ const rssUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
729
+ const items = await fetchRSSFeed(rssUrl);
730
+ return items.filter((i) => new Date(i.publishedAt) > since);
731
+ }
732
+ default:
733
+ return [];
734
+ }
735
+ }
736
+ async function handleAddWatchedSource(args) {
737
+ const { data: { user } } = await supabase.auth.getUser();
738
+ if (!user) return textResult("Error: Not authenticated");
739
+ const { data: existing } = await supabase.from("watched_sources").select("id, display_name").eq("user_id", user.id).eq("handle", args.handle).eq("active", true).maybeSingle();
740
+ if (existing) return textResult(`Already watching "${existing.display_name}"`);
741
+ const { data, error } = await supabase.from("watched_sources").insert({
742
+ user_id: user.id,
743
+ source_type: args.source_type,
744
+ handle: args.handle,
745
+ display_name: args.name || args.handle
746
+ // null = never checked; first scrape will fetch whatever the feed currently contains
747
+ }).select().single();
748
+ if (error) return textResult(`Error: ${error.message}`);
749
+ return textResult(JSON.stringify({
750
+ success: true,
751
+ id: data.id,
752
+ message: `Now watching "${data.display_name}" (${data.source_type}). Call scrape_watched_sources to fetch new items.`
753
+ }, null, 2));
754
+ }
755
+ async function handleListWatchedSources() {
756
+ const { data, error } = await supabase.from("watched_sources").select("id, source_type, handle, display_name, last_checked_at, created_at").eq("active", true).order("created_at", { ascending: false });
757
+ if (error) return textResult(`Error: ${error.message}`);
758
+ if (!data || data.length === 0) return textResult("No watched sources yet. Use add_watched_source to add one.");
759
+ return textResult(JSON.stringify(data, null, 2));
760
+ }
761
+ async function handleRemoveWatchedSource(args) {
762
+ const { error } = await supabase.from("watched_sources").update({ active: false }).eq("id", args.id);
763
+ if (error) return textResult(`Error: ${error.message}`);
764
+ return textResult("Watched source removed.");
765
+ }
766
+ async function handleScrapeWatchedSources(args) {
767
+ const { data: { user } } = await supabase.auth.getUser();
768
+ if (!user) return textResult("Error: Not authenticated");
769
+ let query = supabase.from("watched_sources").select("*").eq("active", true).eq("user_id", user.id);
770
+ if (args.source_id) query = query.eq("id", args.source_id);
771
+ const { data: sources, error } = await query;
772
+ if (error) return textResult(`Error: ${error.message}`);
773
+ if (!sources || sources.length === 0) {
774
+ return textResult("No active watched sources. Use add_watched_source to add one.");
775
+ }
776
+ const countdownExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString();
777
+ const results = [];
778
+ for (const source of sources) {
779
+ let added = 0;
780
+ let skipped = 0;
781
+ try {
782
+ const items = await getSourceItems(source);
783
+ for (const item of items) {
784
+ const { data: dupe } = await supabase.from("bookmarks").select("id").eq("url", item.url).maybeSingle();
785
+ if (dupe) {
786
+ skipped++;
787
+ continue;
788
+ }
789
+ const { error: insertErr } = await supabase.from("bookmarks").insert({
790
+ user_id: user.id,
791
+ url: item.url,
792
+ title: item.title,
793
+ platform: detectPlatform(item.url),
794
+ status: "active",
795
+ countdown_expires_at: countdownExpiresAt,
796
+ content_metadata: {
797
+ author: item.author || source.display_name,
798
+ watched_source_id: source.id,
799
+ watched_source_name: source.display_name
800
+ }
801
+ });
802
+ if (!insertErr) added++;
803
+ }
804
+ await supabase.from("watched_sources").update({ last_checked_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", source.id);
805
+ results.push({ source: source.display_name, type: source.source_type, added, skipped });
806
+ } catch (err) {
807
+ results.push({ source: source.display_name, type: source.source_type, error: err.message });
808
+ }
809
+ }
810
+ const totalAdded = results.reduce((s, r) => s + (r.added || 0), 0);
811
+ return textResult(JSON.stringify({ totalAdded, sources: results }, null, 2));
812
+ }
813
+ server.tool(
814
+ "search_vault",
815
+ "Search your Burn Vault for bookmarks by keyword (searches title, tags, AI takeaway)",
816
+ { query: import_zod.z.string().describe("Search keyword"), limit: import_zod.z.number().optional().describe("Max results (default 10)") },
817
+ rateLimited(handleSearchVault)
818
+ );
819
+ server.tool(
820
+ "list_vault",
821
+ "List bookmarks in your Vault, optionally filtered by category",
822
+ { limit: import_zod.z.number().optional().describe("Max results (default 20)"), category: import_zod.z.string().optional().describe("Filter by vault category") },
823
+ rateLimited(handleListVault)
824
+ );
825
+ server.tool(
826
+ "list_sparks",
827
+ "List your Sparks (bookmarks you have read, with 30-day lifespan). Includes spark insight and expiry date.",
828
+ { limit: import_zod.z.number().optional().describe("Max results (default 20)") },
829
+ rateLimited(handleListSparks)
830
+ );
831
+ server.tool(
832
+ "search_sparks",
833
+ "Search your Sparks by keyword (searches title, tags, AI takeaway, spark insight)",
834
+ { query: import_zod.z.string().describe("Search keyword"), limit: import_zod.z.number().optional().describe("Max results (default 10)") },
835
+ rateLimited(handleSearchSparks)
836
+ );
837
+ server.tool(
838
+ "get_bookmark",
839
+ "Get full details of a single bookmark including AI analysis and extracted content",
840
+ { id: import_zod.z.string().describe("Bookmark UUID") },
841
+ rateLimited(handleGetBookmark)
842
+ );
843
+ server.tool(
844
+ "get_article_content",
845
+ "Get full article content and AI analysis for a bookmark by ID (same as get_bookmark)",
846
+ { id: import_zod.z.string().describe("Bookmark UUID") },
847
+ rateLimited(handleGetArticleContent)
848
+ );
849
+ server.tool(
850
+ "fetch_content",
851
+ "Fetch article/tweet content from a URL. Works with X.com (bypasses GFW via proxy), Reddit, YouTube, Bilibili, WeChat, and any web page. First checks Supabase cache, then fetches live.",
852
+ { url: import_zod.z.string().describe("The URL to fetch content from") },
853
+ rateLimited(handleFetchContent)
854
+ );
855
+ server.tool(
856
+ "list_categories",
857
+ "List all Vault categories with article counts",
858
+ {},
859
+ rateLimited(handleListCategories)
860
+ );
861
+ server.tool(
862
+ "list_flame",
863
+ "List bookmarks in your Flame inbox (24h countdown). Shows AI triage info (strategy, relevance, novelty, hook) and time remaining. Use this to see what needs attention before it burns to Ash.",
864
+ { limit: import_zod.z.number().optional().describe("Max results (default 20)") },
865
+ rateLimited(handleListFlame)
866
+ );
867
+ server.tool(
868
+ "get_flame_detail",
869
+ "Get full details of a Flame bookmark including extracted article content, AI analysis, and reading guidance. Use this to deep-read a bookmark before deciding its fate.",
870
+ { id: import_zod.z.string().describe("Bookmark UUID") },
871
+ rateLimited(handleGetFlameDetail)
872
+ );
873
+ server.tool(
874
+ "get_collections",
875
+ "List all your Collections with article counts and AI overview themes",
876
+ {},
877
+ rateLimited(handleGetCollections)
878
+ );
879
+ server.tool(
880
+ "get_collection_overview",
881
+ "Get a Collection by name with its AI overview and linked bookmarks metadata",
882
+ { name: import_zod.z.string().describe("Collection name") },
883
+ rateLimited(handleGetCollectionOverview)
884
+ );
885
+ server.tool(
886
+ "move_flame_to_spark",
887
+ "Move a Flame bookmark to Spark (mark as worth reading). Sets 30-day Spark lifespan.",
888
+ {
889
+ id: import_zod.z.string().describe("Bookmark UUID"),
890
+ spark_insight: import_zod.z.string().max(500).optional().describe("One-line insight about why this is worth reading")
891
+ },
892
+ rateLimited(handleMoveFlameToSpark)
893
+ );
894
+ server.tool(
895
+ "move_flame_to_ash",
896
+ "Burn a Flame bookmark to Ash (not worth keeping).",
897
+ {
898
+ id: import_zod.z.string().describe("Bookmark UUID"),
899
+ reason: import_zod.z.string().max(200).optional().describe("Why this was burned")
900
+ },
901
+ rateLimited(handleMoveFlameToAsh)
902
+ );
903
+ server.tool(
904
+ "move_spark_to_vault",
905
+ "Promote a Spark bookmark to permanent Vault storage.",
906
+ {
907
+ id: import_zod.z.string().describe("Bookmark UUID"),
908
+ vault_category: import_zod.z.string().max(100).optional().describe("Category to file under in the Vault")
909
+ },
910
+ rateLimited(handleMoveSparkToVault)
911
+ );
912
+ server.tool(
913
+ "move_spark_to_ash",
914
+ "Burn a Spark bookmark to Ash (not valuable enough to vault).",
915
+ {
916
+ id: import_zod.z.string().describe("Bookmark UUID")
917
+ },
918
+ rateLimited(handleMoveSparkToAsh)
919
+ );
920
+ server.tool(
921
+ "batch_triage_flame",
922
+ "Triage multiple Flame bookmarks at once. Each decision moves a bookmark to Spark or Ash.",
923
+ {
924
+ decisions: import_zod.z.array(import_zod.z.object({
925
+ id: import_zod.z.string().describe("Bookmark UUID"),
926
+ action: import_zod.z.enum(["spark", "ash"]).describe("spark = keep, ash = burn"),
927
+ spark_insight: import_zod.z.string().max(500).optional().describe("Insight (only for spark action)")
928
+ })).min(1).max(20).describe("Array of triage decisions")
929
+ },
930
+ rateLimited(handleBatchTriageFlame)
931
+ );
932
+ server.tool(
933
+ "write_bookmark_analysis",
934
+ "Write AI analysis results into a bookmark. Agent analyzes content with its own LLM, then writes structured results back to Burn. Only provided fields are merged \u2014 existing data is preserved.",
935
+ {
936
+ id: import_zod.z.string().describe("Bookmark UUID"),
937
+ analysis: import_zod.z.object({
938
+ ai_summary: import_zod.z.string().max(200).optional().describe("One-line summary"),
939
+ ai_strategy: import_zod.z.enum(["deep_read", "skim", "skip_read", "reference"]).optional().describe("Reading strategy"),
940
+ ai_strategy_reason: import_zod.z.string().max(200).optional().describe("Why this strategy"),
941
+ ai_minutes: import_zod.z.number().int().min(1).max(999).optional().describe("Estimated reading minutes"),
942
+ ai_takeaway: import_zod.z.array(import_zod.z.string().max(200)).max(5).optional().describe("Key takeaways"),
943
+ ai_relevance: import_zod.z.number().int().min(0).max(100).optional().describe("Relevance score 0-100"),
944
+ ai_novelty: import_zod.z.number().int().min(0).max(100).optional().describe("Novelty score 0-100"),
945
+ tags: import_zod.z.array(import_zod.z.string().max(50)).max(10).optional().describe("Topic tags")
946
+ }).describe("Analysis fields to write")
947
+ },
948
+ rateLimited(handleWriteBookmarkAnalysis)
949
+ );
950
+ server.tool(
951
+ "create_collection",
952
+ "Create a new Collection to group related bookmarks together.",
953
+ {
954
+ name: import_zod.z.string().min(1).max(200).describe("Collection name"),
955
+ bookmark_ids: import_zod.z.array(import_zod.z.string()).optional().describe("Initial bookmark UUIDs to include")
956
+ },
957
+ rateLimited(handleCreateCollection)
958
+ );
959
+ server.tool(
960
+ "add_to_collection",
961
+ "Add bookmarks to an existing Collection. Duplicates are silently ignored.",
962
+ {
963
+ collection_id: import_zod.z.string().describe("Collection UUID"),
964
+ bookmark_ids: import_zod.z.array(import_zod.z.string()).min(1).max(50).describe("Bookmark UUIDs to add")
965
+ },
966
+ rateLimited(handleAddToCollection)
967
+ );
968
+ server.tool(
969
+ "remove_from_collection",
970
+ "Remove bookmarks from a Collection.",
971
+ {
972
+ collection_id: import_zod.z.string().describe("Collection UUID"),
973
+ bookmark_ids: import_zod.z.array(import_zod.z.string()).min(1).describe("Bookmark UUIDs to remove")
974
+ },
975
+ rateLimited(handleRemoveFromCollection)
976
+ );
977
+ server.tool(
978
+ "update_collection_overview",
979
+ "Write an AI-generated overview for a Collection (theme, synthesis, patterns, gaps).",
980
+ {
981
+ collection_id: import_zod.z.string().describe("Collection UUID"),
982
+ overview: import_zod.z.object({
983
+ theme: import_zod.z.string().describe("Overarching theme"),
984
+ synthesis: import_zod.z.string().optional().describe("Cross-bookmark synthesis"),
985
+ patterns: import_zod.z.array(import_zod.z.string()).optional().describe("Patterns identified"),
986
+ gaps: import_zod.z.array(import_zod.z.string()).optional().describe("Knowledge gaps identified")
987
+ }).describe("AI-generated overview")
988
+ },
989
+ rateLimited(handleUpdateCollectionOverview)
990
+ );
991
+ server.tool(
992
+ "add_watched_source",
993
+ "Watch an X user, RSS feed, or YouTube channel \u2014 new posts auto-appear in Burn Flame on each scrape.",
994
+ {
995
+ source_type: import_zod.z.enum(["x_user", "rss", "youtube"]).describe("x_user = Twitter/X handle | rss = any RSS/Atom feed URL | youtube = YouTube channel ID"),
996
+ handle: import_zod.z.string().describe('x_user: username without @ (e.g. "karpathy") | rss: full feed URL | youtube: channel ID starting with UC'),
997
+ name: import_zod.z.string().optional().describe("Human-friendly display name (defaults to handle)")
998
+ },
999
+ rateLimited(handleAddWatchedSource)
1000
+ );
1001
+ server.tool(
1002
+ "list_watched_sources",
1003
+ "List all active watched sources (X users, RSS feeds, YouTube channels).",
1004
+ {},
1005
+ rateLimited(handleListWatchedSources)
1006
+ );
1007
+ server.tool(
1008
+ "remove_watched_source",
1009
+ "Stop watching a source. Use list_watched_sources to find the source ID.",
1010
+ { id: import_zod.z.string().describe("Watched source UUID from list_watched_sources") },
1011
+ rateLimited(handleRemoveWatchedSource)
1012
+ );
1013
+ server.tool(
1014
+ "scrape_watched_sources",
1015
+ "Fetch new content from all watched sources (or one specific source) and add new items to Burn Flame. Call this on a schedule or on demand.",
1016
+ { source_id: import_zod.z.string().optional().describe("Scrape only this source ID \u2014 omit to scrape all active sources") },
1017
+ rateLimited(handleScrapeWatchedSources)
1018
+ );
1019
+ server.resource(
1020
+ "vault-bookmarks",
1021
+ "burn://vault/bookmarks",
1022
+ async (uri) => {
1023
+ const { data, error } = await supabase.from("bookmarks").select("*").eq("status", "absorbed").order("created_at", { ascending: false });
1024
+ if (error) {
1025
+ return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: error.message }) }] };
1026
+ }
1027
+ return {
1023
1028
  contents: [{
1024
- uri: uri.href,
1025
- mimeType: 'application/json',
1026
- text: JSON.stringify((data || []).map(metaSummary), null, 2),
1027
- }],
1028
- };
1029
- });
1030
- // ---------------------------------------------------------------------------
1031
- // Resource: burn://vault/categories
1032
- // ---------------------------------------------------------------------------
1033
- server.resource('vault-categories', 'burn://vault/categories', async (uri) => {
1034
- const { data, error } = await supabase
1035
- .from('bookmarks')
1036
- .select('content_metadata')
1037
- .eq('status', 'absorbed');
1038
- if (error) {
1039
- return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: error.message }) }] };
1029
+ uri: uri.href,
1030
+ mimeType: "application/json",
1031
+ text: JSON.stringify((data || []).map(metaSummary), null, 2)
1032
+ }]
1033
+ };
1040
1034
  }
1041
- const counts = {};
1042
- for (const row of data || []) {
1043
- const cat = row.content_metadata?.vault_category || 'Uncategorized';
1035
+ );
1036
+ server.resource(
1037
+ "vault-categories",
1038
+ "burn://vault/categories",
1039
+ async (uri) => {
1040
+ const { data, error } = await supabase.from("bookmarks").select("content_metadata").eq("status", "absorbed");
1041
+ if (error) {
1042
+ return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: error.message }) }] };
1043
+ }
1044
+ const counts = {};
1045
+ for (const row of data || []) {
1046
+ const cat = row.content_metadata?.vault_category || "Uncategorized";
1044
1047
  counts[cat] = (counts[cat] || 0) + 1;
1045
- }
1046
- return {
1048
+ }
1049
+ return {
1047
1050
  contents: [{
1048
- uri: uri.href,
1049
- mimeType: 'application/json',
1050
- text: JSON.stringify(Object.entries(counts).map(([category, count]) => ({ category, count })), null, 2),
1051
- }],
1052
- };
1053
- });
1054
- // ---------------------------------------------------------------------------
1055
- // Start
1056
- // ---------------------------------------------------------------------------
1051
+ uri: uri.href,
1052
+ mimeType: "application/json",
1053
+ text: JSON.stringify(
1054
+ Object.entries(counts).map(([category, count]) => ({ category, count })),
1055
+ null,
1056
+ 2
1057
+ )
1058
+ }]
1059
+ };
1060
+ }
1061
+ );
1062
+ return server;
1063
+ }
1064
+
1065
+ // src/index.ts
1066
+ var SUPABASE_URL = process.env.BURN_SUPABASE_URL || "https://juqtxylquemiuvvmgbej.supabase.co";
1067
+ var SUPABASE_ANON_KEY = process.env.BURN_SUPABASE_ANON_KEY || "sb_publishable_reVgmmCC6ndIo6jFRMM2LQ_wujj5FrO";
1068
+ var MCP_TOKEN = process.env.BURN_MCP_TOKEN;
1069
+ var LEGACY_JWT = process.env.BURN_SUPABASE_TOKEN;
1070
+ if (!MCP_TOKEN && !LEGACY_JWT) {
1071
+ console.error("Error: BURN_MCP_TOKEN environment variable is required.");
1072
+ console.error("Get your token from: Burn App \u2192 Settings \u2192 MCP Server \u2192 Generate Token");
1073
+ process.exit(1);
1074
+ }
1057
1075
  async function main() {
1058
- await initAuth();
1059
- const transport = new stdio_js_1.StdioServerTransport();
1060
- await server.connect(transport);
1061
- console.error('Burn MCP Server running on stdio');
1076
+ const supabase = (0, import_supabase_js.createClient)(SUPABASE_URL, SUPABASE_ANON_KEY, {
1077
+ auth: { persistSession: false, autoRefreshToken: true },
1078
+ ...LEGACY_JWT ? { global: { headers: { Authorization: `Bearer ${LEGACY_JWT}` } } } : {}
1079
+ });
1080
+ if (!LEGACY_JWT && MCP_TOKEN) {
1081
+ const cached = loadCachedSession();
1082
+ if (cached) {
1083
+ try {
1084
+ await applySession(supabase, cached, (s) => saveCachedSession(s));
1085
+ console.error("Burn MCP: restored session from cache");
1086
+ } catch {
1087
+ console.error("Burn MCP: cached session expired, re-exchanging...");
1088
+ const fresh = await exchangeToken(MCP_TOKEN);
1089
+ await applySession(supabase, fresh, (s) => saveCachedSession(s));
1090
+ saveCachedSession(fresh);
1091
+ console.error("Burn MCP: session exchanged and cached");
1092
+ }
1093
+ } else {
1094
+ const fresh = await exchangeToken(MCP_TOKEN);
1095
+ await applySession(supabase, fresh, (s) => saveCachedSession(s));
1096
+ saveCachedSession(fresh);
1097
+ console.error("Burn MCP: session exchanged and cached");
1098
+ }
1099
+ }
1100
+ const server = createBurnServer(supabase);
1101
+ const transport = new import_stdio.StdioServerTransport();
1102
+ await server.connect(transport);
1103
+ console.error("Burn MCP Server running on stdio");
1062
1104
  }
1063
1105
  main().catch((err) => {
1064
- console.error('Fatal error:', err);
1065
- process.exit(1);
1106
+ console.error("Fatal error:", err.message || err);
1107
+ process.exit(1);
1066
1108
  });