@clawchatsai/connector 0.0.86 → 0.0.88

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 (43) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +67 -13
  3. package/dist/index.js +80 -89
  4. package/openclaw.plugin.json +1 -1
  5. package/package.json +11 -8
  6. package/prebuilds/darwin-arm64/node_datachannel.node +0 -0
  7. package/prebuilds/darwin-x64/node_datachannel.node +0 -0
  8. package/prebuilds/linux-arm/node_datachannel.node +0 -0
  9. package/prebuilds/linux-arm64/node_datachannel.node +0 -0
  10. package/prebuilds/linux-x64/node_datachannel.node +0 -0
  11. package/prebuilds/linuxmusl-arm64/node_datachannel.node +0 -0
  12. package/prebuilds/linuxmusl-x64/node_datachannel.node +0 -0
  13. package/prebuilds/win32-arm64/node_datachannel.node +0 -0
  14. package/prebuilds/win32-x64/node_datachannel.node +0 -0
  15. package/server/bootstrap/identity.js +47 -0
  16. package/server/bootstrap/native.js +9 -0
  17. package/server/config.js +63 -0
  18. package/server/controllers/agents.js +20 -0
  19. package/server/controllers/files.js +64 -0
  20. package/server/controllers/filesystem.js +139 -0
  21. package/server/controllers/memory.js +86 -0
  22. package/server/controllers/messages.js +128 -0
  23. package/server/controllers/settings.js +28 -0
  24. package/server/controllers/static.js +56 -0
  25. package/server/controllers/threads.js +113 -0
  26. package/server/controllers/transcribe.js +44 -0
  27. package/server/controllers/workspaces.js +102 -0
  28. package/server/debug.js +56 -0
  29. package/server/gateway-cleanup.js +47 -0
  30. package/server/gateway.js +331 -0
  31. package/server/index.js +397 -0
  32. package/server/providers/memory-config.js +52 -0
  33. package/server/providers/memory.js +108 -0
  34. package/server/store/workspace-store.js +31 -0
  35. package/server/util/context.js +49 -0
  36. package/server/util/helpers.js +111 -0
  37. package/server/util/http.js +57 -0
  38. package/server/util/multipart.js +46 -0
  39. package/dist/migrate.d.ts +0 -16
  40. package/dist/migrate.js +0 -114
  41. package/dist/updater.d.ts +0 -21
  42. package/dist/updater.js +0 -64
  43. package/server.js +0 -2459
@@ -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
+ }
package/dist/migrate.d.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Schema migration runner for the ClawChats plugin SQLite database.
3
- *
4
- * - Tracks schema version in a `_schema_version` table
5
- * - Runs migrations sequentially from current version to target
6
- * - Creates a backup before migrating (if not `:memory:` and currentVersion > 0)
7
- * - Wraps all migrations in a transaction with rollback on error
8
- */
9
- import type Database from 'better-sqlite3';
10
- export declare const SCHEMA_VERSION = 1;
11
- /**
12
- * Run all pending migrations against the given database.
13
- *
14
- * Safe to call on every startup — exits early if already up to date.
15
- */
16
- export declare function runMigrations(db: Database): void;
package/dist/migrate.js DELETED
@@ -1,114 +0,0 @@
1
- /**
2
- * Schema migration runner for the ClawChats plugin SQLite database.
3
- *
4
- * - Tracks schema version in a `_schema_version` table
5
- * - Runs migrations sequentially from current version to target
6
- * - Creates a backup before migrating (if not `:memory:` and currentVersion > 0)
7
- * - Wraps all migrations in a transaction with rollback on error
8
- */
9
- import * as fs from 'node:fs';
10
- export const SCHEMA_VERSION = 1;
11
- const migrations = [
12
- {
13
- version: 1,
14
- up: (db) => {
15
- db.exec(`
16
- CREATE TABLE IF NOT EXISTS threads (
17
- id TEXT PRIMARY KEY,
18
- session_key TEXT UNIQUE NOT NULL,
19
- title TEXT DEFAULT 'New chat',
20
- pinned INTEGER DEFAULT 0,
21
- pin_order INTEGER DEFAULT 0,
22
- model TEXT,
23
- last_session_id TEXT,
24
- sort_order INTEGER DEFAULT 0,
25
- unread_count INTEGER DEFAULT 0,
26
- created_at INTEGER NOT NULL,
27
- updated_at INTEGER NOT NULL
28
- );
29
-
30
- CREATE TABLE IF NOT EXISTS messages (
31
- id TEXT PRIMARY KEY,
32
- thread_id TEXT NOT NULL,
33
- role TEXT NOT NULL,
34
- content TEXT NOT NULL,
35
- status TEXT DEFAULT 'sent',
36
- metadata TEXT,
37
- seq INTEGER,
38
- timestamp INTEGER NOT NULL,
39
- created_at INTEGER NOT NULL,
40
- FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
41
- );
42
-
43
- CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id, timestamp);
44
- CREATE INDEX IF NOT EXISTS idx_messages_dedup ON messages(thread_id, role, timestamp);
45
-
46
- CREATE TABLE IF NOT EXISTS unread_messages (
47
- thread_id TEXT NOT NULL,
48
- message_id TEXT NOT NULL,
49
- created_at INTEGER NOT NULL,
50
- PRIMARY KEY (thread_id, message_id),
51
- FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE
52
- );
53
-
54
- CREATE INDEX IF NOT EXISTS idx_unread_thread ON unread_messages(thread_id);
55
-
56
- CREATE VIRTUAL TABLE messages_fts USING fts5(
57
- content, content=messages, content_rowid=rowid,
58
- tokenize='porter unicode61'
59
- );
60
-
61
- CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
62
- INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
63
- END;
64
- `);
65
- },
66
- },
67
- ];
68
- /**
69
- * Run all pending migrations against the given database.
70
- *
71
- * Safe to call on every startup — exits early if already up to date.
72
- */
73
- export function runMigrations(db) {
74
- // Ensure the version-tracking table exists
75
- db.exec(`
76
- CREATE TABLE IF NOT EXISTS _schema_version (
77
- version INTEGER NOT NULL
78
- );
79
- `);
80
- // Read the current schema version (0 = fresh database)
81
- const row = db.prepare('SELECT version FROM _schema_version LIMIT 1').get();
82
- const currentVersion = row?.version ?? 0;
83
- if (currentVersion >= SCHEMA_VERSION) {
84
- return;
85
- }
86
- // Create a backup before migrating an existing database
87
- const dbPath = db.name;
88
- if (currentVersion > 0 && dbPath !== ':memory:') {
89
- const backupPath = `${dbPath}.backup-v${currentVersion}`;
90
- fs.copyFileSync(dbPath, backupPath);
91
- }
92
- // Run all pending migrations inside a single transaction
93
- const migrate = db.transaction(() => {
94
- for (const migration of migrations) {
95
- if (migration.version > currentVersion) {
96
- migration.up(db);
97
- }
98
- }
99
- // Record the new schema version
100
- if (row === undefined) {
101
- db.prepare('INSERT INTO _schema_version (version) VALUES (?)').run(SCHEMA_VERSION);
102
- }
103
- else {
104
- db.prepare('UPDATE _schema_version SET version = ?').run(SCHEMA_VERSION);
105
- }
106
- });
107
- try {
108
- migrate();
109
- }
110
- catch (err) {
111
- // better-sqlite3 transactions automatically roll back on throw
112
- throw err;
113
- }
114
- }
package/dist/updater.d.ts DELETED
@@ -1,21 +0,0 @@
1
- /**
2
- * Auto-update checker for the @clawchatsai/connector plugin.
3
- *
4
- * Checks the npm registry for newer versions and can trigger
5
- * an in-place update via the OpenClaw plugin CLI.
6
- */
7
- export interface UpdateInfo {
8
- current: string;
9
- latest: string;
10
- }
11
- /**
12
- * Check npm registry for updates.
13
- * Returns UpdateInfo if a newer version is available, null otherwise.
14
- * Silently returns null on any network or parse error.
15
- */
16
- export declare function checkForUpdates(): Promise<UpdateInfo | null>;
17
- /**
18
- * Run the OpenClaw plugin update command.
19
- * Throws an Error if the command exits with a non-zero code or times out.
20
- */
21
- export declare function performUpdate(): Promise<void>;
package/dist/updater.js DELETED
@@ -1,64 +0,0 @@
1
- /**
2
- * Auto-update checker for the @clawchatsai/connector plugin.
3
- *
4
- * Checks the npm registry for newer versions and can trigger
5
- * an in-place update via the OpenClaw plugin CLI.
6
- */
7
- import { execFile } from 'node:child_process';
8
- import { PLUGIN_VERSION } from './index.js';
9
- /**
10
- * Compare two semver strings. Returns true if `a` is greater than `b`.
11
- * Handles major.minor.patch only — no pre-release suffixes.
12
- */
13
- function semverGt(a, b) {
14
- const parse = (v) => {
15
- const parts = v.split('.').map(Number);
16
- return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
17
- };
18
- const [aMaj, aMin, aPat] = parse(a);
19
- const [bMaj, bMin, bPat] = parse(b);
20
- if (aMaj !== bMaj)
21
- return aMaj > bMaj;
22
- if (aMin !== bMin)
23
- return aMin > bMin;
24
- return aPat > bPat;
25
- }
26
- /**
27
- * Check npm registry for updates.
28
- * Returns UpdateInfo if a newer version is available, null otherwise.
29
- * Silently returns null on any network or parse error.
30
- */
31
- export async function checkForUpdates() {
32
- try {
33
- const res = await fetch('https://registry.npmjs.org/@clawchatsai%2Fconnector/latest');
34
- if (!res.ok)
35
- return null;
36
- const data = await res.json();
37
- const latest = data.version;
38
- if (typeof latest !== 'string')
39
- return null;
40
- if (!semverGt(latest, PLUGIN_VERSION))
41
- return null;
42
- return { current: PLUGIN_VERSION, latest };
43
- }
44
- catch {
45
- return null;
46
- }
47
- }
48
- /**
49
- * Run the OpenClaw plugin update command.
50
- * Throws an Error if the command exits with a non-zero code or times out.
51
- */
52
- export async function performUpdate() {
53
- return new Promise((resolve, reject) => {
54
- const child = execFile('openclaw', ['plugins', 'update', '@clawchatsai/connector'], { timeout: 120_000 }, (error) => {
55
- if (error) {
56
- reject(new Error(`Plugin update failed: ${error.message}`));
57
- }
58
- else {
59
- resolve();
60
- }
61
- });
62
- child; // reference kept to satisfy linter — callback handles lifecycle
63
- });
64
- }