@clawchatsai/connector 0.0.37 → 0.0.39

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +107 -95
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "type": "module",
5
5
  "description": "ClawChats OpenClaw plugin — P2P tunnel + local API bridge",
6
6
  "main": "dist/index.js",
package/server.js CHANGED
@@ -296,6 +296,26 @@ function getActiveDb() {
296
296
  return getDb(getWorkspaces().active);
297
297
  }
298
298
 
299
+ let _globalDb = null;
300
+ function getGlobalDb() {
301
+ if (_globalDb) return _globalDb;
302
+ fs.mkdirSync(DATA_DIR, { recursive: true });
303
+ const dbPath = path.join(DATA_DIR, 'global.db');
304
+ _globalDb = new Database(dbPath);
305
+ _globalDb.pragma('journal_mode = WAL');
306
+ _globalDb.exec(`
307
+ CREATE TABLE IF NOT EXISTS custom_emojis (
308
+ name TEXT NOT NULL,
309
+ pack TEXT NOT NULL DEFAULT 'slackmojis',
310
+ url TEXT NOT NULL,
311
+ mime_type TEXT,
312
+ created_at INTEGER DEFAULT (strftime('%s','now')),
313
+ PRIMARY KEY (name, pack)
314
+ )
315
+ `);
316
+ return _globalDb;
317
+ }
318
+
299
319
  function closeDb(workspaceName) {
300
320
  const db = dbCache.get(workspaceName);
301
321
  if (db) {
@@ -309,6 +329,7 @@ function closeAllDbs() {
309
329
  db.close();
310
330
  }
311
331
  dbCache.clear();
332
+ if (_globalDb) { _globalDb.close(); _globalDb = null; }
312
333
  }
313
334
 
314
335
  function migrate(db) {
@@ -1445,15 +1466,32 @@ function handleWorkspaceFileRead(req, res, query) {
1445
1466
  return sendError(res, 404, 'File not found');
1446
1467
  }
1447
1468
 
1448
- // Limit file size to 1MB
1449
1469
  const stat = fs.statSync(resolved);
1450
- if (stat.size > 1024 * 1024) {
1451
- return sendError(res, 413, 'File too large (max 1MB)');
1470
+ const ext = path.extname(resolved).toLowerCase().slice(1);
1471
+
1472
+ // MIME type map — binary types get served as binary, rest as text
1473
+ const mimeMap = {
1474
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
1475
+ gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
1476
+ bmp: 'image/bmp', ico: 'image/x-icon',
1477
+ pdf: 'application/pdf',
1478
+ mp3: 'audio/mpeg', mp4: 'video/mp4', wav: 'audio/wav',
1479
+ ogg: 'audio/ogg', webm: 'video/webm',
1480
+ };
1481
+ const mime = mimeMap[ext];
1482
+ const isBinary = !!mime;
1483
+
1484
+ if (isBinary) {
1485
+ if (stat.size > 20 * 1024 * 1024) return sendError(res, 413, 'File too large (max 20MB)');
1486
+ const content = fs.readFileSync(resolved);
1487
+ res.writeHead(200, { 'Content-Type': mime, 'Cache-Control': 'private, max-age=60' });
1488
+ res.end(content);
1489
+ } else {
1490
+ if (stat.size > 1024 * 1024) return sendError(res, 413, 'File too large (max 1MB)');
1491
+ const content = fs.readFileSync(resolved, 'utf8');
1492
+ res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
1493
+ res.end(content);
1452
1494
  }
1453
-
1454
- const content = fs.readFileSync(resolved, 'utf8');
1455
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
1456
- res.end(content);
1457
1495
  }
1458
1496
 
1459
1497
  async function handleWorkspaceFileWrite(req, res, query) {
@@ -2090,28 +2128,11 @@ async function handleRequest(req, res) {
2090
2128
 
2091
2129
  // Custom emoji listing (no auth — public like static assets)
2092
2130
  if (method === 'GET' && urlPath === '/api/emoji') {
2093
- const emojiDir = path.join(__dirname, 'emoji');
2094
- if (!fs.existsSync(emojiDir)) {
2095
- res.writeHead(200, { 'Content-Type': 'application/json' });
2096
- return res.end('[]');
2097
- }
2098
2131
  try {
2099
- const packs = fs.readdirSync(emojiDir).filter(d =>
2100
- fs.statSync(path.join(emojiDir, d)).isDirectory()
2101
- );
2102
- const result = [];
2103
- for (const pack of packs) {
2104
- const packDir = path.join(emojiDir, pack);
2105
- const files = fs.readdirSync(packDir).filter(f =>
2106
- /\.(png|gif|webp|jpg|jpeg)$/i.test(f)
2107
- );
2108
- for (const file of files) {
2109
- const name = file.replace(/\.[^.]+$/, '');
2110
- result.push({ name, pack, path: `/emoji/${pack}/${file}` });
2111
- }
2112
- }
2132
+ const db = getGlobalDb();
2133
+ const rows = db.prepare('SELECT name, pack, url, mime_type FROM custom_emojis ORDER BY created_at DESC').all();
2113
2134
  res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' });
2114
- return res.end(JSON.stringify(result));
2135
+ return res.end(JSON.stringify(rows));
2115
2136
  } catch (e) {
2116
2137
  res.writeHead(500, { 'Content-Type': 'application/json' });
2117
2138
  return res.end(JSON.stringify({ error: e.message }));
@@ -2161,7 +2182,7 @@ async function handleRequest(req, res) {
2161
2182
  }
2162
2183
  }
2163
2184
 
2164
- // Download emoji from URL and save to emoji/ directory
2185
+ // Add custom emoji (store URL in global.db)
2165
2186
  if (method === 'POST' && urlPath === '/api/emoji/add') {
2166
2187
  if (!checkAuth(req, res)) return;
2167
2188
  try {
@@ -2170,45 +2191,40 @@ async function handleRequest(req, res) {
2170
2191
  res.writeHead(400, { 'Content-Type': 'application/json' });
2171
2192
  return res.end(JSON.stringify({ error: 'Missing url or name' }));
2172
2193
  }
2173
- const targetPack = pack || 'custom';
2174
- const emojiDir = path.join(__dirname, 'emoji', targetPack);
2175
- if (!fs.existsSync(emojiDir)) fs.mkdirSync(emojiDir, { recursive: true });
2194
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
2195
+ const targetPack = pack || 'slackmojis';
2196
+ // Determine mime type from URL extension
2197
+ const urlLower = url.split('?')[0].toLowerCase();
2198
+ let mimeType = 'image/png';
2199
+ if (urlLower.endsWith('.gif')) mimeType = 'image/gif';
2200
+ else if (urlLower.endsWith('.webp')) mimeType = 'image/webp';
2201
+ else if (urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg')) mimeType = 'image/jpeg';
2202
+
2203
+ const db = getGlobalDb();
2204
+ db.prepare('INSERT OR REPLACE INTO custom_emojis (name, pack, url, mime_type) VALUES (?, ?, ?, ?)')
2205
+ .run(safeName, targetPack, url, mimeType);
2176
2206
 
2177
- // Follow redirects and download
2178
- const https = await import('https');
2179
- const http = await import('http');
2180
- const download = (downloadUrl, redirects = 0) => new Promise((resolve, reject) => {
2181
- if (redirects > 5) return reject(new Error('Too many redirects'));
2182
- const mod = downloadUrl.startsWith('https') ? https.default : http.default;
2183
- mod.get(downloadUrl, (resp) => {
2184
- if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) {
2185
- return resolve(download(resp.headers.location, redirects + 1));
2186
- }
2187
- if (resp.statusCode !== 200) return reject(new Error(`HTTP ${resp.statusCode}`));
2188
- const chunks = [];
2189
- resp.on('data', chunk => chunks.push(chunk));
2190
- resp.on('end', () => resolve({ buffer: Buffer.concat(chunks), contentType: resp.headers['content-type'] || '' }));
2191
- }).on('error', reject);
2192
- });
2207
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2208
+ return res.end(JSON.stringify({ name: safeName, pack: targetPack, url, mime_type: mimeType }));
2209
+ } catch (e) {
2210
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2211
+ return res.end(JSON.stringify({ error: e.message }));
2212
+ }
2213
+ }
2193
2214
 
2194
- const { buffer, contentType } = await download(url);
2195
- // Determine extension from content-type or URL
2196
- let ext = 'png';
2197
- if (contentType.includes('gif')) ext = 'gif';
2198
- else if (contentType.includes('webp')) ext = 'webp';
2199
- else if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = 'jpg';
2200
- else {
2201
- const urlExt = url.split('?')[0].split('.').pop().toLowerCase();
2202
- if (['png', 'gif', 'webp', 'jpg', 'jpeg'].includes(urlExt)) ext = urlExt;
2215
+ // Delete custom emoji
2216
+ if (method === 'DELETE' && urlPath === '/api/emoji') {
2217
+ if (!checkAuth(req, res)) return;
2218
+ try {
2219
+ const { name, pack } = await parseBody(req);
2220
+ if (!name || !pack) {
2221
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2222
+ return res.end(JSON.stringify({ error: 'Missing name or pack' }));
2203
2223
  }
2204
-
2205
- const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
2206
- const filePath = path.join(emojiDir, `${safeName}.${ext}`);
2207
- fs.writeFileSync(filePath, buffer);
2208
-
2209
- const result = { name: safeName, pack: targetPack, path: `/emoji/${targetPack}/${safeName}.${ext}` };
2224
+ const db = getGlobalDb();
2225
+ db.prepare('DELETE FROM custom_emojis WHERE name = ? AND pack = ?').run(name, pack);
2210
2226
  res.writeHead(200, { 'Content-Type': 'application/json' });
2211
- return res.end(JSON.stringify(result));
2227
+ return res.end(JSON.stringify({ ok: true }));
2212
2228
  } catch (e) {
2213
2229
  res.writeHead(500, { 'Content-Type': 'application/json' });
2214
2230
  return res.end(JSON.stringify({ error: e.message }));
@@ -4285,17 +4301,11 @@ export function createApp(config = {}) {
4285
4301
 
4286
4302
  // Custom emoji listing (no auth)
4287
4303
  if (method === 'GET' && urlPath === '/api/emoji') {
4288
- const emojiDir = path.join(__dirname, 'emoji');
4289
- if (!fs.existsSync(emojiDir)) { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end('[]'); }
4290
4304
  try {
4291
- const packs = fs.readdirSync(emojiDir).filter(d => fs.statSync(path.join(emojiDir, d)).isDirectory());
4292
- const result = [];
4293
- for (const pack of packs) {
4294
- const files = fs.readdirSync(path.join(emojiDir, pack)).filter(f => /\.(png|gif|webp|jpg|jpeg)$/i.test(f));
4295
- for (const file of files) result.push({ name: file.replace(/\.[^.]+$/, ''), pack, path: `/emoji/${pack}/${file}` });
4296
- }
4305
+ const db = getGlobalDb();
4306
+ const rows = db.prepare('SELECT name, pack, url, mime_type FROM custom_emojis ORDER BY created_at DESC').all();
4297
4307
  res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' });
4298
- return res.end(JSON.stringify(result));
4308
+ return res.end(JSON.stringify(rows));
4299
4309
  } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4300
4310
  }
4301
4311
 
@@ -4324,35 +4334,34 @@ export function createApp(config = {}) {
4324
4334
 
4325
4335
  if (!_checkAuth(req, res)) return;
4326
4336
 
4327
- // Download emoji from URL (auth required)
4337
+ // Add custom emoji (auth required)
4328
4338
  if (method === 'POST' && urlPath === '/api/emoji/add') {
4329
4339
  try {
4330
4340
  const { url, name, pack } = await parseBody(req);
4331
4341
  if (!url || !name) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing url or name' })); }
4332
- const targetPack = pack || 'custom';
4333
- const emojiDir = path.join(__dirname, 'emoji', targetPack);
4334
- if (!fs.existsSync(emojiDir)) fs.mkdirSync(emojiDir, { recursive: true });
4335
- const https = await import('https');
4336
- const http = await import('http');
4337
- const download = (dlUrl, redirects = 0) => new Promise((resolve, reject) => {
4338
- if (redirects > 5) return reject(new Error('Too many redirects'));
4339
- const mod = dlUrl.startsWith('https') ? https.default : http.default;
4340
- mod.get(dlUrl, (resp) => {
4341
- if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) return resolve(download(resp.headers.location, redirects + 1));
4342
- if (resp.statusCode !== 200) return reject(new Error(`HTTP ${resp.statusCode}`));
4343
- const chunks = []; resp.on('data', c => chunks.push(c)); resp.on('end', () => resolve({ buffer: Buffer.concat(chunks), contentType: resp.headers['content-type'] || '' }));
4344
- }).on('error', reject);
4345
- });
4346
- const { buffer, contentType } = await download(url);
4347
- let ext = 'png';
4348
- if (contentType.includes('gif')) ext = 'gif';
4349
- else if (contentType.includes('webp')) ext = 'webp';
4350
- else if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = 'jpg';
4351
- else { const u = url.split('?')[0].split('.').pop().toLowerCase(); if (['png','gif','webp','jpg','jpeg'].includes(u)) ext = u; }
4352
4342
  const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
4353
- fs.writeFileSync(path.join(emojiDir, `${safeName}.${ext}`), buffer);
4343
+ const targetPack = pack || 'slackmojis';
4344
+ const urlLower = url.split('?')[0].toLowerCase();
4345
+ let mimeType = 'image/png';
4346
+ if (urlLower.endsWith('.gif')) mimeType = 'image/gif';
4347
+ else if (urlLower.endsWith('.webp')) mimeType = 'image/webp';
4348
+ else if (urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg')) mimeType = 'image/jpeg';
4349
+ const db = getGlobalDb();
4350
+ db.prepare('INSERT OR REPLACE INTO custom_emojis (name, pack, url, mime_type) VALUES (?, ?, ?, ?)').run(safeName, targetPack, url, mimeType);
4351
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4352
+ return res.end(JSON.stringify({ name: safeName, pack: targetPack, url, mime_type: mimeType }));
4353
+ } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4354
+ }
4355
+
4356
+ // Delete custom emoji (auth required)
4357
+ if (method === 'DELETE' && urlPath === '/api/emoji') {
4358
+ try {
4359
+ const { name, pack } = await parseBody(req);
4360
+ if (!name || !pack) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing name or pack' })); }
4361
+ const db = getGlobalDb();
4362
+ db.prepare('DELETE FROM custom_emojis WHERE name = ? AND pack = ?').run(name, pack);
4354
4363
  res.writeHead(200, { 'Content-Type': 'application/json' });
4355
- return res.end(JSON.stringify({ name: safeName, pack: targetPack, path: `/emoji/${targetPack}/${safeName}.${ext}` }));
4364
+ return res.end(JSON.stringify({ ok: true }));
4356
4365
  } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4357
4366
  }
4358
4367
 
@@ -4503,6 +4512,9 @@ if (isDirectRun) {
4503
4512
 
4504
4513
  // Connect to gateway
4505
4514
  app.gatewayClient.connect();
4515
+
4516
+ // Initialize global DB (custom emojis, etc.)
4517
+ getGlobalDb();
4506
4518
  });
4507
4519
 
4508
4520
  // Graceful shutdown