@clawchatsai/connector 0.0.86 → 0.0.87

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.
@@ -0,0 +1,111 @@
1
+ export function syncThreadUnreadCount(db, threadId) {
2
+ const count = db.prepare('SELECT COUNT(*) as c FROM unread_messages WHERE thread_id = ?').get(threadId).c;
3
+ db.prepare('UPDATE threads SET unread_count = ? WHERE id = ?').run(count, threadId);
4
+ return count;
5
+ }
6
+
7
+ export function parseSessionKey(sessionKey) {
8
+ if (!sessionKey) return null;
9
+ const match = sessionKey.match(/^agent:([^:]+):([^:]+):chat:([^:]+)$/);
10
+ if (!match) return null;
11
+ return { agent: match[1], workspace: match[2], threadId: match[3] };
12
+ }
13
+
14
+ export function extractContent(message) {
15
+ if (!message) return '';
16
+ if (typeof message.content === 'string') return message.content;
17
+ if (Array.isArray(message.content)) {
18
+ return message.content.filter(p => p.type === 'text').map(p => p.text).join('');
19
+ }
20
+ return '';
21
+ }
22
+
23
+ export function isSilentReplyExact(text, token = 'NO_REPLY') {
24
+ if (!text) return false;
25
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
26
+ return new RegExp(`^\\s*${escaped}\\s*$`).test(text);
27
+ }
28
+
29
+ export function isSilentReplyPrefix(text, token = 'NO_REPLY') {
30
+ if (!text) return false;
31
+ const trimmed = text.trimStart();
32
+ if (!trimmed) return false;
33
+ if (trimmed !== trimmed.toUpperCase()) return false;
34
+ const normalized = trimmed.toUpperCase();
35
+ if (normalized.length < 2 || /[^A-Z_]/.test(normalized)) return false;
36
+ const tokenUpper = token.toUpperCase();
37
+ if (!tokenUpper.startsWith(normalized)) return false;
38
+ if (normalized.includes('_')) return true;
39
+ return tokenUpper === 'NO_REPLY' && normalized === 'NO';
40
+ }
41
+
42
+ export function stripTrailingSentinel(text, token = 'NO_REPLY') {
43
+ if (!text) return text;
44
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
45
+ return text.replace(new RegExp(`(?:^|\\s+|\\*+)${escaped}\\s*$`), '').trim();
46
+ }
47
+
48
+ export function stripFinalTags(text) {
49
+ return text ? text.replace(/<\s*\/?\s*final\s*>/gi, '') : text;
50
+ }
51
+
52
+ export function sanitizeAssistantContent(text) {
53
+ if (!text) return text;
54
+ let out = stripFinalTags(text);
55
+ out = out.replace(/^(?:[ \t]*\r?\n)+/, '');
56
+ if (out.includes('NO_REPLY')) out = stripTrailingSentinel(out, 'NO_REPLY');
57
+ if (out.includes('HEARTBEAT_OK')) out = stripTrailingSentinel(out, 'HEARTBEAT_OK');
58
+ return out;
59
+ }
60
+
61
+ export function generateActivitySummary(steps) {
62
+ const toolSteps = steps.filter(s => s.type === 'tool' && s.phase !== 'result' && s.phase !== 'update');
63
+ const hasThinking = steps.some(s => s.type === 'thinking' && s.text);
64
+ const hasNarration = steps.some(s => s.type === 'assistant' && s.text?.trim());
65
+ if (toolSteps.length === 0 && !hasThinking && !hasNarration) return null;
66
+ if (toolSteps.length === 0 && hasThinking) return 'Reasoned through the problem';
67
+ if (toolSteps.length === 0 && hasNarration) return 'Processed in multiple steps';
68
+ const counts = {};
69
+ for (const s of toolSteps) { const name = s.name || 'unknown'; counts[name] = (counts[name] || 0) + 1; }
70
+ const toolNames = { web_search: 'searched the web', web_fetch: 'fetched web pages', Read: 'read files', read: 'read files', Write: 'wrote files', write: 'wrote files', Edit: 'edited files', edit: 'edited files', exec: 'ran commands', Bash: 'ran commands', browser: 'browsed the web', memory_search: 'searched memory', memory_store: 'saved to memory', image: 'analyzed images', message: 'sent messages', sessions_spawn: 'spawned sub-agents', cron: 'managed cron jobs', Grep: 'searched code', grep: 'searched code', Glob: 'found files', glob: 'found files' };
71
+ const parts = [];
72
+ for (const [name, count] of Object.entries(counts)) {
73
+ const friendly = toolNames[name];
74
+ parts.push(friendly ? (count > 1 ? `${friendly} (${count}×)` : friendly) : (count > 1 ? `used ${name} (${count}×)` : `used ${name}`));
75
+ }
76
+ if (parts.length === 0) return null;
77
+ if (parts.length === 1) return parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
78
+ const last = parts.pop();
79
+ return (parts.join(', ') + ' and ' + last).replace(/^./, c => c.toUpperCase());
80
+ }
81
+
82
+ export function writeActivityToDb(getDbFn, broadcastFn, runId, log) {
83
+ if (!log._parsed) log._parsed = parseSessionKey(log.sessionKey);
84
+ const parsed = log._parsed;
85
+ if (!parsed) return;
86
+ const db = getDbFn(parsed.workspace);
87
+ if (!db) return;
88
+ const cleanSteps = log.steps.map(s => { const c = { ...s }; delete c._sealed; return c; });
89
+ const summary = generateActivitySummary(log.steps);
90
+ const now = Date.now();
91
+ if (!log._messageId) {
92
+ const thread = db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId);
93
+ if (!thread) return;
94
+ const messageId = `gw-activity-${runId}`;
95
+ const metadata = { activityLog: cleanSteps, activitySummary: summary, pending: true };
96
+ try {
97
+ db.prepare(`INSERT OR IGNORE INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at) VALUES (?, ?, 'assistant', '', 'sent', ?, ?, ?)`).run(messageId, parsed.threadId, JSON.stringify(metadata), now, now);
98
+ log._messageId = messageId;
99
+ broadcastFn(JSON.stringify({ type: 'clawchats', event: 'message-saved', threadId: parsed.threadId, workspace: parsed.workspace, messageId, timestamp: now }));
100
+ } catch (err) {
101
+ console.error(`[activity] Failed to write activity ${messageId}:`, err.message);
102
+ }
103
+ } else {
104
+ const existing = db.prepare('SELECT metadata FROM messages WHERE id = ?').get(log._messageId);
105
+ const metadata = existing?.metadata ? JSON.parse(existing.metadata) : {};
106
+ metadata.activityLog = cleanSteps;
107
+ metadata.activitySummary = summary;
108
+ metadata.pending = true;
109
+ db.prepare('UPDATE messages SET metadata = ? WHERE id = ?').run(JSON.stringify(metadata), log._messageId);
110
+ }
111
+ }
@@ -0,0 +1,57 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export function parseBody(req) {
4
+ return new Promise((resolve, reject) => {
5
+ const chunks = [];
6
+ req.on('data', c => chunks.push(c));
7
+ req.on('end', () => {
8
+ const raw = Buffer.concat(chunks).toString();
9
+ if (!raw) return resolve({});
10
+ try { resolve(JSON.parse(raw)); }
11
+ catch { reject(new Error('Invalid JSON')); }
12
+ });
13
+ req.on('error', reject);
14
+ });
15
+ }
16
+
17
+ export function send(res, status, data) {
18
+ const body = JSON.stringify(data);
19
+ res.writeHead(status, {
20
+ 'Content-Type': 'application/json',
21
+ 'Access-Control-Allow-Origin': '*',
22
+ 'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
23
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
24
+ });
25
+ res.end(body);
26
+ }
27
+
28
+ export function sendError(res, status, message) {
29
+ send(res, status, { error: message });
30
+ }
31
+
32
+ export function setCors(res) {
33
+ res.setHeader('Access-Control-Allow-Origin', '*');
34
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
35
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
36
+ }
37
+
38
+ export function uuid() {
39
+ return crypto.randomUUID();
40
+ }
41
+
42
+ export function matchRoute(method, url, pattern) {
43
+ const [pMethod, pPath] = pattern.split(' ');
44
+ if (method !== pMethod) return null;
45
+ const pParts = pPath.split('/').filter(Boolean);
46
+ const uParts = url.split('/').filter(Boolean);
47
+ if (pParts.length !== uParts.length) return null;
48
+ const params = {};
49
+ for (let i = 0; i < pParts.length; i++) {
50
+ if (pParts[i].startsWith(':')) {
51
+ params[pParts[i].slice(1)] = decodeURIComponent(uParts[i]);
52
+ } else if (pParts[i] !== uParts[i]) {
53
+ return null;
54
+ }
55
+ }
56
+ return params;
57
+ }
@@ -0,0 +1,46 @@
1
+ export function parseMultipart(req) {
2
+ return new Promise((resolve, reject) => {
3
+ const contentType = req.headers['content-type'] || '';
4
+ const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
5
+ if (!boundaryMatch) return reject(new Error('No boundary in content-type'));
6
+ const boundary = boundaryMatch[1] || boundaryMatch[2];
7
+ const chunks = [];
8
+ req.on('data', c => chunks.push(c));
9
+ req.on('end', () => {
10
+ const buf = Buffer.concat(chunks);
11
+ const files = [];
12
+ const delimiter = Buffer.from(`--${boundary}`);
13
+
14
+ let pos = 0;
15
+ while (pos < buf.length) {
16
+ const start = buf.indexOf(delimiter, pos);
17
+ if (start === -1) break;
18
+ const nextStart = buf.indexOf(delimiter, start + delimiter.length);
19
+ if (nextStart === -1) break;
20
+
21
+ const part = buf.subarray(start + delimiter.length, nextStart);
22
+ const headerEnd = part.indexOf('\r\n\r\n');
23
+ if (headerEnd === -1) { pos = nextStart; continue; }
24
+
25
+ const headerStr = part.subarray(0, headerEnd).toString();
26
+ let body = part.subarray(headerEnd + 4);
27
+ if (body.length >= 2 && body[body.length - 2] === 0x0d && body[body.length - 1] === 0x0a) {
28
+ body = body.subarray(0, body.length - 2);
29
+ }
30
+
31
+ const filenameMatch = headerStr.match(/filename="([^"]+)"/);
32
+ const ctMatch = headerStr.match(/Content-Type:\s*(\S+)/i);
33
+ if (filenameMatch) {
34
+ files.push({
35
+ filename: filenameMatch[1],
36
+ mimeType: ctMatch ? ctMatch[1] : 'application/octet-stream',
37
+ data: body,
38
+ });
39
+ }
40
+ pos = nextStart;
41
+ }
42
+ resolve(files);
43
+ });
44
+ req.on('error', reject);
45
+ });
46
+ }