@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 +20 -2
- package/dist/shim.d.ts +1 -0
- package/dist/shim.js +1 -0
- package/package.json +1 -1
- package/server.js +211 -7
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:
|
|
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,
|
|
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
package/dist/shim.js
CHANGED
package/package.json
CHANGED
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
|
-
|
|
3253
|
-
|
|
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);
|