@clawchatsai/connector 0.0.30 → 0.0.32

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.32",
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
@@ -742,6 +742,9 @@ async function handleUpdateWorkspace(req, res, params) {
742
742
  if (body.icon !== undefined) {
743
743
  ws.workspaces[params.name].icon = body.icon;
744
744
  }
745
+ if (body.lastThread !== undefined) {
746
+ ws.workspaces[params.name].lastThread = body.lastThread;
747
+ }
745
748
  setWorkspaces(ws);
746
749
  send(res, 200, { workspace: ws.workspaces[params.name] });
747
750
  }
@@ -2037,16 +2040,17 @@ async function handleRequest(req, res) {
2037
2040
  const isIcon = urlPath.startsWith('/icons/');
2038
2041
  const isLib = urlPath.startsWith('/lib/');
2039
2042
  const isFrontend = urlPath.startsWith('/frontend/');
2043
+ const isEmoji = urlPath.startsWith('/emoji/');
2040
2044
  const isConfig = urlPath === '/config.js';
2041
2045
  const staticPath = fileName ? path.join(__dirname, fileName)
2042
- : (isIcon || isLib || isFrontend || isConfig) ? path.join(__dirname, urlPath.slice(1))
2046
+ : (isIcon || isLib || isFrontend || isEmoji || isConfig) ? path.join(__dirname, urlPath.slice(1))
2043
2047
  : null;
2044
2048
  if (staticPath && fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) {
2045
2049
  const ext = path.extname(staticPath).toLowerCase();
2046
2050
  const mimeMap = {
2047
2051
  '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
2048
2052
  '.json': 'application/json', '.ico': 'image/x-icon',
2049
- '.png': 'image/png', '.svg': 'image/svg+xml',
2053
+ '.png': 'image/png', '.svg': 'image/svg+xml', '.gif': 'image/gif', '.webp': 'image/webp',
2050
2054
  };
2051
2055
  const ct = mimeMap[ext] || 'application/octet-stream';
2052
2056
  const stat = fs.statSync(staticPath);
@@ -2065,6 +2069,133 @@ async function handleRequest(req, res) {
2065
2069
  return handleServeUpload(req, res, p);
2066
2070
  }
2067
2071
 
2072
+ // Custom emoji listing (no auth — public like static assets)
2073
+ if (method === 'GET' && urlPath === '/api/emoji') {
2074
+ const emojiDir = path.join(__dirname, 'emoji');
2075
+ if (!fs.existsSync(emojiDir)) {
2076
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2077
+ return res.end('[]');
2078
+ }
2079
+ try {
2080
+ const packs = fs.readdirSync(emojiDir).filter(d =>
2081
+ fs.statSync(path.join(emojiDir, d)).isDirectory()
2082
+ );
2083
+ const result = [];
2084
+ for (const pack of packs) {
2085
+ const packDir = path.join(emojiDir, pack);
2086
+ const files = fs.readdirSync(packDir).filter(f =>
2087
+ /\.(png|gif|webp|jpg|jpeg)$/i.test(f)
2088
+ );
2089
+ for (const file of files) {
2090
+ const name = file.replace(/\.[^.]+$/, '');
2091
+ result.push({ name, pack, path: `/emoji/${pack}/${file}` });
2092
+ }
2093
+ }
2094
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' });
2095
+ return res.end(JSON.stringify(result));
2096
+ } catch (e) {
2097
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2098
+ return res.end(JSON.stringify({ error: e.message }));
2099
+ }
2100
+ }
2101
+
2102
+ // Search slackmojis.com (scrapes HTML search since JSON API has no search)
2103
+ if (method === 'GET' && urlPath === '/api/emoji/search') {
2104
+ const q = query.q || '';
2105
+ if (!q) {
2106
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2107
+ return res.end(JSON.stringify({ error: 'Missing ?q= parameter' }));
2108
+ }
2109
+ try {
2110
+ const https = await import('https');
2111
+ const fetchUrl = `https://slackmojis.com/emojis/search?query=${encodeURIComponent(q)}`;
2112
+ const html = await new Promise((resolve, reject) => {
2113
+ https.default.get(fetchUrl, (resp) => {
2114
+ let body = '';
2115
+ resp.on('data', chunk => body += chunk);
2116
+ resp.on('end', () => resolve(body));
2117
+ }).on('error', reject);
2118
+ });
2119
+ // Parse emoji entries from HTML:
2120
+ // <li class='emoji name' title='name'>
2121
+ // <a ... data-emoji-id="123" data-emoji-id-name="123-name" href="/emojis/123-name/download">
2122
+ // <img ... src="https://emojis.slackmojis.com/emojis/images/.../name.png?..." />
2123
+ const results = [];
2124
+ const regex = /data-emoji-id-name="([^"]+)"[^>]*href="([^"]+)"[\s\S]*?<img[^>]*src="([^"]+)"/g;
2125
+ let match;
2126
+ while ((match = regex.exec(html)) !== null && results.length < 50) {
2127
+ const idName = match[1]; // e.g. "57350-sextant"
2128
+ const downloadPath = match[2];
2129
+ const imageUrl = match[3];
2130
+ const name = idName.replace(/^\d+-/, '');
2131
+ results.push({
2132
+ name,
2133
+ image_url: imageUrl,
2134
+ download_url: `https://slackmojis.com${downloadPath}`,
2135
+ });
2136
+ }
2137
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2138
+ return res.end(JSON.stringify(results));
2139
+ } catch (e) {
2140
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2141
+ return res.end(JSON.stringify({ error: e.message }));
2142
+ }
2143
+ }
2144
+
2145
+ // Download emoji from URL and save to emoji/ directory
2146
+ if (method === 'POST' && urlPath === '/api/emoji/add') {
2147
+ if (!checkAuth(req, res)) return;
2148
+ try {
2149
+ const { url, name, pack } = await parseBody(req);
2150
+ if (!url || !name) {
2151
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2152
+ return res.end(JSON.stringify({ error: 'Missing url or name' }));
2153
+ }
2154
+ const targetPack = pack || 'custom';
2155
+ const emojiDir = path.join(__dirname, 'emoji', targetPack);
2156
+ if (!fs.existsSync(emojiDir)) fs.mkdirSync(emojiDir, { recursive: true });
2157
+
2158
+ // Follow redirects and download
2159
+ const https = await import('https');
2160
+ const http = await import('http');
2161
+ const download = (downloadUrl, redirects = 0) => new Promise((resolve, reject) => {
2162
+ if (redirects > 5) return reject(new Error('Too many redirects'));
2163
+ const mod = downloadUrl.startsWith('https') ? https.default : http.default;
2164
+ mod.get(downloadUrl, (resp) => {
2165
+ if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) {
2166
+ return resolve(download(resp.headers.location, redirects + 1));
2167
+ }
2168
+ if (resp.statusCode !== 200) return reject(new Error(`HTTP ${resp.statusCode}`));
2169
+ const chunks = [];
2170
+ resp.on('data', chunk => chunks.push(chunk));
2171
+ resp.on('end', () => resolve({ buffer: Buffer.concat(chunks), contentType: resp.headers['content-type'] || '' }));
2172
+ }).on('error', reject);
2173
+ });
2174
+
2175
+ const { buffer, contentType } = await download(url);
2176
+ // Determine extension from content-type or URL
2177
+ let ext = 'png';
2178
+ if (contentType.includes('gif')) ext = 'gif';
2179
+ else if (contentType.includes('webp')) ext = 'webp';
2180
+ else if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = 'jpg';
2181
+ else {
2182
+ const urlExt = url.split('?')[0].split('.').pop().toLowerCase();
2183
+ if (['png', 'gif', 'webp', 'jpg', 'jpeg'].includes(urlExt)) ext = urlExt;
2184
+ }
2185
+
2186
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
2187
+ const filePath = path.join(emojiDir, `${safeName}.${ext}`);
2188
+ fs.writeFileSync(filePath, buffer);
2189
+
2190
+ const result = { name: safeName, pack: targetPack, path: `/emoji/${targetPack}/${safeName}.${ext}` };
2191
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2192
+ return res.end(JSON.stringify(result));
2193
+ } catch (e) {
2194
+ res.writeHead(500, { 'Content-Type': 'application/json' });
2195
+ return res.end(JSON.stringify({ error: e.message }));
2196
+ }
2197
+ }
2198
+
2068
2199
  // Auth check for all other routes
2069
2200
  if (!checkAuth(req, res)) return;
2070
2201
 
@@ -3048,6 +3179,7 @@ export function createApp(config = {}) {
3048
3179
  if (body.label !== undefined) ws.workspaces[params.name].label = body.label;
3049
3180
  if (body.color !== undefined) ws.workspaces[params.name].color = body.color;
3050
3181
  if (body.icon !== undefined) ws.workspaces[params.name].icon = body.icon;
3182
+ if (body.lastThread !== undefined) ws.workspaces[params.name].lastThread = body.lastThread;
3051
3183
  _setWorkspaces(ws);
3052
3184
  send(res, 200, { workspace: ws.workspaces[params.name] });
3053
3185
  }
@@ -3247,10 +3379,14 @@ export function createApp(config = {}) {
3247
3379
  return sendError(res, 400, 'Required: id, role, content, timestamp');
3248
3380
  }
3249
3381
  const metadata = body.metadata ? JSON.stringify(body.metadata) : null;
3250
- const existing = db.prepare('SELECT id, status FROM messages WHERE id = ?').get(body.id);
3382
+ const existing = db.prepare('SELECT id, status, metadata FROM messages WHERE id = ?').get(body.id);
3251
3383
  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);
3384
+ const newStatus = body.status || existing.status;
3385
+ const statusChanged = body.status && body.status !== existing.status;
3386
+ if (statusChanged || metadata) {
3387
+ // Only overwrite metadata if new metadata is provided; otherwise preserve existing
3388
+ const finalMetadata = metadata || existing.metadata || null;
3389
+ db.prepare('UPDATE messages SET status = ?, content = ?, metadata = ? WHERE id = ?').run(newStatus, body.content, finalMetadata, body.id);
3254
3390
  }
3255
3391
  } else {
3256
3392
  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 +4005,12 @@ export function createApp(config = {}) {
3869
4005
  const isIcon = urlPath.startsWith('/icons/');
3870
4006
  const isLib = urlPath.startsWith('/lib/');
3871
4007
  const isFrontend = urlPath.startsWith('/frontend/');
4008
+ const isEmoji = urlPath.startsWith('/emoji/');
3872
4009
  const isConfig = urlPath === '/config.js';
3873
- const staticPath = fileName ? path.join(__dirname, fileName) : (isIcon || isLib || isFrontend || isConfig) ? path.join(__dirname, urlPath.slice(1)) : null;
4010
+ const staticPath = fileName ? path.join(__dirname, fileName) : (isIcon || isLib || isFrontend || isEmoji || isConfig) ? path.join(__dirname, urlPath.slice(1)) : null;
3874
4011
  if (staticPath && fs.existsSync(staticPath) && fs.statSync(staticPath).isFile()) {
3875
4012
  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' };
4013
+ 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
4014
  const ct = mimeMap[ext] || 'application/octet-stream';
3878
4015
  const stat = fs.statSync(staticPath);
3879
4016
  res.writeHead(200, { 'Content-Type': ct, 'Content-Length': stat.size, 'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600' });
@@ -3884,8 +4021,79 @@ export function createApp(config = {}) {
3884
4021
  let p;
3885
4022
  if ((p = matchRoute(method, urlPath, 'GET /api/uploads/:threadId/:fileId'))) return _handleServeUpload(req, res, p);
3886
4023
 
4024
+ // Custom emoji listing (no auth)
4025
+ if (method === 'GET' && urlPath === '/api/emoji') {
4026
+ const emojiDir = path.join(__dirname, 'emoji');
4027
+ if (!fs.existsSync(emojiDir)) { res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end('[]'); }
4028
+ try {
4029
+ const packs = fs.readdirSync(emojiDir).filter(d => fs.statSync(path.join(emojiDir, d)).isDirectory());
4030
+ const result = [];
4031
+ for (const pack of packs) {
4032
+ const files = fs.readdirSync(path.join(emojiDir, pack)).filter(f => /\.(png|gif|webp|jpg|jpeg)$/i.test(f));
4033
+ for (const file of files) result.push({ name: file.replace(/\.[^.]+$/, ''), pack, path: `/emoji/${pack}/${file}` });
4034
+ }
4035
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' });
4036
+ return res.end(JSON.stringify(result));
4037
+ } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4038
+ }
4039
+
4040
+ // Search slackmojis.com (no auth, proxied)
4041
+ if (method === 'GET' && urlPath === '/api/emoji/search') {
4042
+ const q = query.q || '';
4043
+ if (!q) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing ?q=' })); }
4044
+ try {
4045
+ const https = await import('https');
4046
+ const html = await new Promise((resolve, reject) => {
4047
+ https.default.get(`https://slackmojis.com/emojis/search?query=${encodeURIComponent(q)}`, (resp) => {
4048
+ let body = ''; resp.on('data', c => body += c); resp.on('end', () => resolve(body));
4049
+ }).on('error', reject);
4050
+ });
4051
+ const results = [];
4052
+ const regex = /data-emoji-id-name="([^"]+)"[^>]*href="([^"]+)"[\s\S]*?<img[^>]*src="([^"]+)"/g;
4053
+ let match;
4054
+ while ((match = regex.exec(html)) !== null && results.length < 50) {
4055
+ const name = match[1].replace(/^\d+-/, '');
4056
+ results.push({ name, image_url: match[3], download_url: `https://slackmojis.com${match[2]}` });
4057
+ }
4058
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4059
+ return res.end(JSON.stringify(results));
4060
+ } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4061
+ }
4062
+
3887
4063
  if (!_checkAuth(req, res)) return;
3888
4064
 
4065
+ // Download emoji from URL (auth required)
4066
+ if (method === 'POST' && urlPath === '/api/emoji/add') {
4067
+ try {
4068
+ const { url, name, pack } = await parseBody(req);
4069
+ if (!url || !name) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing url or name' })); }
4070
+ const targetPack = pack || 'custom';
4071
+ const emojiDir = path.join(__dirname, 'emoji', targetPack);
4072
+ if (!fs.existsSync(emojiDir)) fs.mkdirSync(emojiDir, { recursive: true });
4073
+ const https = await import('https');
4074
+ const http = await import('http');
4075
+ const download = (dlUrl, redirects = 0) => new Promise((resolve, reject) => {
4076
+ if (redirects > 5) return reject(new Error('Too many redirects'));
4077
+ const mod = dlUrl.startsWith('https') ? https.default : http.default;
4078
+ mod.get(dlUrl, (resp) => {
4079
+ if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) return resolve(download(resp.headers.location, redirects + 1));
4080
+ if (resp.statusCode !== 200) return reject(new Error(`HTTP ${resp.statusCode}`));
4081
+ const chunks = []; resp.on('data', c => chunks.push(c)); resp.on('end', () => resolve({ buffer: Buffer.concat(chunks), contentType: resp.headers['content-type'] || '' }));
4082
+ }).on('error', reject);
4083
+ });
4084
+ const { buffer, contentType } = await download(url);
4085
+ let ext = 'png';
4086
+ if (contentType.includes('gif')) ext = 'gif';
4087
+ else if (contentType.includes('webp')) ext = 'webp';
4088
+ else if (contentType.includes('jpeg') || contentType.includes('jpg')) ext = 'jpg';
4089
+ else { const u = url.split('?')[0].split('.').pop().toLowerCase(); if (['png','gif','webp','jpg','jpeg'].includes(u)) ext = u; }
4090
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_');
4091
+ fs.writeFileSync(path.join(emojiDir, `${safeName}.${ext}`), buffer);
4092
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4093
+ return res.end(JSON.stringify({ name: safeName, pack: targetPack, path: `/emoji/${targetPack}/${safeName}.${ext}` }));
4094
+ } catch (e) { res.writeHead(500, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: e.message })); }
4095
+ }
4096
+
3889
4097
  try {
3890
4098
  if (method === 'GET' && urlPath === '/api/file') return handleServeFile(req, res, query);
3891
4099
  if (method === 'GET' && urlPath === '/api/workspace') return handleWorkspaceList(req, res, query);