@clawchatsai/connector 0.0.30 → 0.0.31

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
@@ -488,15 +488,33 @@ async function handleRpcMessage(dc, msg, ctx) {
488
488
  };
489
489
  try {
490
490
  const response = await dispatchRpc(rpcReq, app.handleRequest);
491
+ // For binary content types (images, audio, etc.), wrap as _binary envelope
492
+ // so the browser transport can reconstruct a proper Blob with correct MIME type
493
+ const contentType = response.headers['content-type'] || '';
494
+ const isBinaryResponse = /^(image|audio|video|application\/octet-stream|application\/pdf)/.test(contentType);
495
+ let responseBody;
496
+ if (isBinaryResponse && response.rawBody) {
497
+ // Encode raw bytes as base64 in a _binary envelope.
498
+ // The transport layer on the browser side already handles this format,
499
+ // reconstructing a proper Blob with the correct MIME type.
500
+ responseBody = {
501
+ _binary: true,
502
+ contentType,
503
+ data: response.rawBody.toString('base64'),
504
+ };
505
+ }
506
+ else {
507
+ responseBody = response.body;
508
+ }
491
509
  const responseMsg = {
492
510
  type: 'rpc-res',
493
511
  id: response.id,
494
512
  status: response.status,
495
- body: response.body,
513
+ body: responseBody,
496
514
  };
497
515
  const responseStr = JSON.stringify(responseMsg);
498
516
  if (responseStr.length > MAX_DC_MESSAGE_SIZE) {
499
- sendChunked(dc, response.id, response.status, response.body);
517
+ sendChunked(dc, response.id, response.status, JSON.stringify(responseBody));
500
518
  }
501
519
  else {
502
520
  dc.send(responseStr);
package/dist/shim.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface RpcResponse {
20
20
  status: number;
21
21
  headers: Record<string, string>;
22
22
  body: string;
23
+ rawBody?: Buffer;
23
24
  }
24
25
  type HandleRequestFn = (req: FakeReq, res: FakeRes) => void | Promise<void>;
25
26
  /**
package/dist/shim.js CHANGED
@@ -150,5 +150,6 @@ export async function dispatchRpc(rpc, handleRequest) {
150
150
  status: result.status,
151
151
  headers: result.headers,
152
152
  body: tryJsonParse(result.body),
153
+ rawBody: result.body, // Preserve raw buffer for binary encoding
153
154
  };
154
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.30",
3
+ "version": "0.0.31",
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
@@ -2037,16 +2037,17 @@ async function handleRequest(req, res) {
2037
2037
  const isIcon = urlPath.startsWith('/icons/');
2038
2038
  const isLib = urlPath.startsWith('/lib/');
2039
2039
  const isFrontend = urlPath.startsWith('/frontend/');
2040
+ const isEmoji = urlPath.startsWith('/emoji/');
2040
2041
  const isConfig = urlPath === '/config.js';
2041
2042
  const staticPath = fileName ? path.join(__dirname, fileName)
2042
- : (isIcon || isLib || isFrontend || isConfig) ? path.join(__dirname, urlPath.slice(1))
2043
+ : (isIcon || isLib || isFrontend || isEmoji || isConfig) ? path.join(__dirname, urlPath.slice(1))
2043
2044
  : null;
2044
2045
  if (staticPath && fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) {
2045
2046
  const ext = path.extname(staticPath).toLowerCase();
2046
2047
  const mimeMap = {
2047
2048
  '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
2048
2049
  '.json': 'application/json', '.ico': 'image/x-icon',
2049
- '.png': 'image/png', '.svg': 'image/svg+xml',
2050
+ '.png': 'image/png', '.svg': 'image/svg+xml', '.gif': 'image/gif', '.webp': 'image/webp',
2050
2051
  };
2051
2052
  const ct = mimeMap[ext] || 'application/octet-stream';
2052
2053
  const stat = fs.statSync(staticPath);
@@ -2065,6 +2066,133 @@ async function handleRequest(req, res) {
2065
2066
  return handleServeUpload(req, res, p);
2066
2067
  }
2067
2068
 
2069
+ // Custom emoji listing (no auth — public like static assets)
2070
+ if (method === 'GET' && urlPath === '/api/emoji') {
2071
+ const emojiDir = path.join(__dirname, 'emoji');
2072
+ if (!fs.existsSync(emojiDir)) {
2073
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2074
+ return res.end('[]');
2075
+ }
2076
+ try {
2077
+ const packs = fs.readdirSync(emojiDir).filter(d =>
2078
+ fs.statSync(path.join(emojiDir, d)).isDirectory()
2079
+ );
2080
+ const result = [];
2081
+ for (const pack of packs) {
2082
+ const packDir = path.join(emojiDir, pack);
2083
+ const files = fs.readdirSync(packDir).filter(f =>
2084
+ /\.(png|gif|webp|jpg|jpeg)$/i.test(f)
2085
+ );
2086
+ for (const file of files) {
2087
+ const name = file.replace(/\.[^.]+$/, '');
2088
+ result.push({ name, pack, path: `/emoji/${pack}/${file}` });
2089
+ }
2090
+ }
2091
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' });
2092
+ return res.end(JSON.stringify(result));
2093
+ } catch (e) {
2094
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2095
+ return res.end(JSON.stringify({ error: e.message }));
2096
+ }
2097
+ }
2098
+
2099
+ // Search slackmojis.com (scrapes HTML search since JSON API has no search)
2100
+ if (method === 'GET' && urlPath === '/api/emoji/search') {
2101
+ const q = query.q || '';
2102
+ if (!q) {
2103
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2104
+ return res.end(JSON.stringify({ error: 'Missing ?q= parameter' }));
2105
+ }
2106
+ try {
2107
+ const https = await import('https');
2108
+ const fetchUrl = `https://slackmojis.com/emojis/search?query=${encodeURIComponent(q)}`;
2109
+ const html = await new Promise((resolve, reject) => {
2110
+ https.default.get(fetchUrl, (resp) => {
2111
+ let body = '';
2112
+ resp.on('data', chunk => body += chunk);
2113
+ resp.on('end', () => resolve(body));
2114
+ }).on('error', reject);
2115
+ });
2116
+ // Parse emoji entries from HTML:
2117
+ // <li class='emoji name' title='name'>
2118
+ // <a ... data-emoji-id="123" data-emoji-id-name="123-name" href="/emojis/123-name/download">
2119
+ // <img ... src="https://emojis.slackmojis.com/emojis/images/.../name.png?..." />
2120
+ const results = [];
2121
+ const regex = /data-emoji-id-name="([^"]+)"[^>]*href="([^"]+)"[\s\S]*?<img[^>]*src="([^"]+)"/g;
2122
+ let match;
2123
+ while ((match = regex.exec(html)) !== null && results.length < 50) {
2124
+ const idName = match[1]; // e.g. "57350-sextant"
2125
+ const downloadPath = match[2];
2126
+ const imageUrl = match[3];
2127
+ const name = idName.replace(/^\d+-/, '');
2128
+ results.push({
2129
+ name,
2130
+ image_url: imageUrl,
2131
+ download_url: `https://slackmojis.com${downloadPath}`,
2132
+ });
2133
+ }
2134
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2135
+ return res.end(JSON.stringify(results));
2136
+ } catch (e) {
2137
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2138
+ return res.end(JSON.stringify({ error: e.message }));
2139
+ }
2140
+ }
2141
+
2142
+ // Download emoji from URL and save to emoji/ directory
2143
+ if (method === 'POST' && urlPath === '/api/emoji/add') {
2144
+ if (!checkAuth(req, res)) return;
2145
+ try {
2146
+ const { url, name, pack } = await parseBody(req);
2147
+ if (!url || !name) {
2148
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2149
+ return res.end(JSON.stringify({ error: 'Missing url or name' }));
2150
+ }
2151
+ const targetPack = pack || 'custom';
2152
+ const emojiDir = path.join(__dirname, 'emoji', targetPack);
2153
+ if (!fs.existsSync(emojiDir)) fs.mkdirSync(emojiDir, { recursive: true });
2154
+
2155
+ // Follow redirects and download
2156
+ const https = await import('https');
2157
+ const http = await import('http');
2158
+ const download = (downloadUrl, redirects = 0) => new Promise((resolve, reject) => {
2159
+ if (redirects > 5) return reject(new Error('Too many redirects'));
2160
+ const mod = downloadUrl.startsWith('https') ? https.default : http.default;
2161
+ mod.get(downloadUrl, (resp) => {
2162
+ if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) {
2163
+ return resolve(download(resp.headers.location, redirects + 1));
2164
+ }
2165
+ if (resp.statusCode !== 200) return reject(new Error(`HTTP ${resp.statusCode}`));
2166
+ const chunks = [];
2167
+ resp.on('data', chunk => chunks.push(chunk));
2168
+ resp.on('end', () => resolve({ buffer: Buffer.concat(chunks), contentType: resp.headers['content-type'] || '' }));
2169
+ }).on('error', reject);
2170
+ });
2171
+
2172
+ const { buffer, contentType } = await download(url);
2173
+ // Determine extension from content-type or URL
2174
+ let ext = 'png';
2175
+ if (contentType.includes('gif')) ext = 'gif';
2176
+ else if (contentType.includes('webp')) ext = 'webp';
2177
+ else if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = 'jpg';
2178
+ else {
2179
+ const urlExt = url.split('?')[0].split('.').pop().toLowerCase();
2180
+ if (['png', 'gif', 'webp', 'jpg', 'jpeg'].includes(urlExt)) ext = urlExt;
2181
+ }
2182
+
2183
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
2184
+ const filePath = path.join(emojiDir, `${safeName}.${ext}`);
2185
+ fs.writeFileSync(filePath, buffer);
2186
+
2187
+ const result = { name: safeName, pack: targetPack, path: `/emoji/${targetPack}/${safeName}.${ext}` };
2188
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2189
+ return res.end(JSON.stringify(result));
2190
+ } catch (e) {
2191
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2192
+ return res.end(JSON.stringify({ error: e.message }));
2193
+ }
2194
+ }
2195
+
2068
2196
  // Auth check for all other routes
2069
2197
  if (!checkAuth(req, res)) return;
2070
2198
 
@@ -3247,10 +3375,14 @@ export function createApp(config = {}) {
3247
3375
  return sendError(res, 400, 'Required: id, role, content, timestamp');
3248
3376
  }
3249
3377
  const metadata = body.metadata ? JSON.stringify(body.metadata) : null;
3250
- const existing = db.prepare('SELECT id, status FROM messages WHERE id = ?').get(body.id);
3378
+ const existing = db.prepare('SELECT id, status, metadata FROM messages WHERE id = ?').get(body.id);
3251
3379
  if (existing) {
3252
- if (body.status && body.status !== existing.status) {
3253
- db.prepare('UPDATE messages SET status = ?, content = ?, metadata = ? WHERE id = ?').run(body.status, body.content, metadata, body.id);
3380
+ const newStatus = body.status || existing.status;
3381
+ const statusChanged = body.status && body.status !== existing.status;
3382
+ if (statusChanged || metadata) {
3383
+ // Only overwrite metadata if new metadata is provided; otherwise preserve existing
3384
+ const finalMetadata = metadata || existing.metadata || null;
3385
+ db.prepare('UPDATE messages SET status = ?, content = ?, metadata = ? WHERE id = ?').run(newStatus, body.content, finalMetadata, body.id);
3254
3386
  }
3255
3387
  } else {
3256
3388
  db.prepare('INSERT INTO messages (id, thread_id, role, content, status, metadata, seq, timestamp, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)').run(body.id, params.id, body.role, body.content, body.status || 'sent', metadata, body.seq || null, body.timestamp, Date.now());
@@ -3869,11 +4001,12 @@ export function createApp(config = {}) {
3869
4001
  const isIcon = urlPath.startsWith('/icons/');
3870
4002
  const isLib = urlPath.startsWith('/lib/');
3871
4003
  const isFrontend = urlPath.startsWith('/frontend/');
4004
+ const isEmoji = urlPath.startsWith('/emoji/');
3872
4005
  const isConfig = urlPath === '/config.js';
3873
- const staticPath = fileName ? path.join(__dirname, fileName) : (isIcon || isLib || isFrontend || isConfig) ? path.join(__dirname, urlPath.slice(1)) : null;
4006
+ const staticPath = fileName ? path.join(__dirname, fileName) : (isIcon || isLib || isFrontend || isEmoji || isConfig) ? path.join(__dirname, urlPath.slice(1)) : null;
3874
4007
  if (staticPath && fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) {
3875
4008
  const ext = path.extname(staticPath).toLowerCase();
3876
- const mimeMap = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.ico': 'image/x-icon', '.png': 'image/png', '.svg': 'image/svg+xml' };
4009
+ const mimeMap = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.ico': 'image/x-icon', '.png': 'image/png', '.svg': 'image/svg+xml', '.gif': 'image/gif', '.webp': 'image/webp' };
3877
4010
  const ct = mimeMap[ext] || 'application/octet-stream';
3878
4011
  const stat = fs.statSync(staticPath);
3879
4012
  res.writeHead(200, { 'Content-Type': ct, 'Content-Length': stat.size, 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600' });
@@ -3884,8 +4017,79 @@ export function createApp(config = {}) {
3884
4017
  let p;
3885
4018
  if ((p = matchRoute(method, urlPath, 'GET /api/uploads/:threadId/:fileId'))) return _handleServeUpload(req, res, p);
3886
4019
 
4020
+ // Custom emoji listing (no auth)
4021
+ if (method === 'GET' && urlPath === '/api/emoji') {
4022
+ const emojiDir = path.join(__dirname, 'emoji');
4023
+ if (!fs.existsSync(emojiDir)) { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end('[]'); }
4024
+ try {
4025
+ const packs = fs.readdirSync(emojiDir).filter(d => fs.statSync(path.join(emojiDir, d)).isDirectory());
4026
+ const result = [];
4027
+ for (const pack of packs) {
4028
+ const files = fs.readdirSync(path.join(emojiDir, pack)).filter(f => /\.(png|gif|webp|jpg|jpeg)$/i.test(f));
4029
+ for (const file of files) result.push({ name: file.replace(/\.[^.]+$/, ''), pack, path: `/emoji/${pack}/${file}` });
4030
+ }
4031
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' });
4032
+ return res.end(JSON.stringify(result));
4033
+ } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4034
+ }
4035
+
4036
+ // Search slackmojis.com (no auth, proxied)
4037
+ if (method === 'GET' && urlPath === '/api/emoji/search') {
4038
+ const q = query.q || '';
4039
+ if (!q) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing ?q=' })); }
4040
+ try {
4041
+ const https = await import('https');
4042
+ const html = await new Promise((resolve, reject) => {
4043
+ https.default.get(`https://slackmojis.com/emojis/search?query=${encodeURIComponent(q)}`, (resp) => {
4044
+ let body = ''; resp.on('data', c => body += c); resp.on('end', () => resolve(body));
4045
+ }).on('error', reject);
4046
+ });
4047
+ const results = [];
4048
+ const regex = /data-emoji-id-name="([^"]+)"[^>]*href="([^"]+)"[\s\S]*?<img[^>]*src="([^"]+)"/g;
4049
+ let match;
4050
+ while ((match = regex.exec(html)) !== null && results.length < 50) {
4051
+ const name = match[1].replace(/^\d+-/, '');
4052
+ results.push({ name, image_url: match[3], download_url: `https://slackmojis.com${match[2]}` });
4053
+ }
4054
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4055
+ return res.end(JSON.stringify(results));
4056
+ } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4057
+ }
4058
+
3887
4059
  if (!_checkAuth(req, res)) return;
3888
4060
 
4061
+ // Download emoji from URL (auth required)
4062
+ if (method === 'POST' && urlPath === '/api/emoji/add') {
4063
+ try {
4064
+ const { url, name, pack } = await parseBody(req);
4065
+ if (!url || !name) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing url or name' })); }
4066
+ const targetPack = pack || 'custom';
4067
+ const emojiDir = path.join(__dirname, 'emoji', targetPack);
4068
+ if (!fs.existsSync(emojiDir)) fs.mkdirSync(emojiDir, { recursive: true });
4069
+ const https = await import('https');
4070
+ const http = await import('http');
4071
+ const download = (dlUrl, redirects = 0) => new Promise((resolve, reject) => {
4072
+ if (redirects > 5) return reject(new Error('Too many redirects'));
4073
+ const mod = dlUrl.startsWith('https') ? https.default : http.default;
4074
+ mod.get(dlUrl, (resp) => {
4075
+ if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) return resolve(download(resp.headers.location, redirects + 1));
4076
+ if (resp.statusCode !== 200) return reject(new Error(`HTTP ${resp.statusCode}`));
4077
+ const chunks = []; resp.on('data', c => chunks.push(c)); resp.on('end', () => resolve({ buffer: Buffer.concat(chunks), contentType: resp.headers['content-type'] || '' }));
4078
+ }).on('error', reject);
4079
+ });
4080
+ const { buffer, contentType } = await download(url);
4081
+ let ext = 'png';
4082
+ if (contentType.includes('gif')) ext = 'gif';
4083
+ else if (contentType.includes('webp')) ext = 'webp';
4084
+ else if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = 'jpg';
4085
+ else { const u = url.split('?')[0].split('.').pop().toLowerCase(); if (['png','gif','webp','jpg','jpeg'].includes(u)) ext = u; }
4086
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
4087
+ fs.writeFileSync(path.join(emojiDir, `${safeName}.${ext}`), buffer);
4088
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4089
+ return res.end(JSON.stringify({ name: safeName, pack: targetPack, path: `/emoji/${targetPack}/${safeName}.${ext}` }));
4090
+ } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4091
+ }
4092
+
3889
4093
  try {
3890
4094
  if (method === 'GET' && urlPath === '/api/file') return handleServeFile(req, res, query);
3891
4095
  if (method === 'GET' && urlPath === '/api/workspace') return handleWorkspaceList(req, res, query);