@clawchatsai/connector 0.0.46 → 0.0.48
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.d.ts +9 -0
- package/dist/index.js +38 -2
- package/package.json +1 -1
- package/server.js +16 -68
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,15 @@ interface PluginApi {
|
|
|
47
47
|
text: string;
|
|
48
48
|
};
|
|
49
49
|
}) => void;
|
|
50
|
+
registerHook: (events: string | string[], handler: (event: Record<string, unknown>, ctx: Record<string, unknown>) => void | Promise<void>, opts: {
|
|
51
|
+
name: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
}) => void;
|
|
54
|
+
registerTypedHook: (hookName: string, handler: (event: Record<string, unknown>, ctx: Record<string, unknown>) => void | Promise<void>, opts?: {
|
|
55
|
+
name?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
priority?: number;
|
|
58
|
+
}) => void;
|
|
50
59
|
runtime: {
|
|
51
60
|
requestRestart?: (reason: string) => void;
|
|
52
61
|
};
|
package/dist/index.js
CHANGED
|
@@ -123,7 +123,7 @@ async function ensureNativeModules(ctx) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
async function startClawChats(ctx, api) {
|
|
126
|
+
async function startClawChats(ctx, api, mediaStash) {
|
|
127
127
|
_stopRequested = false;
|
|
128
128
|
let config = loadConfig();
|
|
129
129
|
if (!config) {
|
|
@@ -175,6 +175,7 @@ async function startClawChats(ctx, api) {
|
|
|
175
175
|
gatewayUrl: 'ws://localhost:18789',
|
|
176
176
|
authToken: '', // P2P: DataChannel is the auth boundary (signaling authenticates both sides)
|
|
177
177
|
gatewayToken, // For WS auth to local OpenClaw gateway
|
|
178
|
+
mediaStash, // Shared Map populated by after_tool_call hook (captures MEDIA: paths from exec)
|
|
178
179
|
});
|
|
179
180
|
// 4. Connect createApp's gateway client (handles persistence + event relay)
|
|
180
181
|
app.gatewayClient.connect();
|
|
@@ -958,10 +959,45 @@ const plugin = {
|
|
|
958
959
|
name: 'ClawChats',
|
|
959
960
|
description: 'Connects your gateway to ClawChats via WebRTC P2P',
|
|
960
961
|
register(api) {
|
|
962
|
+
// Shared stash: after_tool_call hook populates this; saveAssistantMessage reads + clears it.
|
|
963
|
+
// Keyed by sessionKey → absolute paths extracted from MEDIA: lines in exec stdout.
|
|
964
|
+
const mediaStash = new Map();
|
|
965
|
+
// Capture MEDIA: paths from exec tool results before the gateway strips them from message text.
|
|
966
|
+
// Fires after every tool call; we filter to exec only and check for MEDIA: lines.
|
|
967
|
+
api.registerTypedHook('after_tool_call', (event, ctx) => {
|
|
968
|
+
// Diagnostic: log every tool call so we can confirm hook fires + see real tool names
|
|
969
|
+
console.log(`[clawchats] hook:after_tool_call toolName=${String(event.toolName)} sessionKey=${String(ctx.sessionKey)}`);
|
|
970
|
+
if (event.toolName !== 'exec')
|
|
971
|
+
return;
|
|
972
|
+
// result is { content: [{ type: 'text', text: '...' }] } — extract text defensively
|
|
973
|
+
const result = event.result;
|
|
974
|
+
let text = '';
|
|
975
|
+
if (typeof result?.text === 'string') {
|
|
976
|
+
text = result.text;
|
|
977
|
+
}
|
|
978
|
+
else if (Array.isArray(result?.content)) {
|
|
979
|
+
text = result.content
|
|
980
|
+
.filter(c => c.type === 'text' && typeof c.text === 'string')
|
|
981
|
+
.map(c => c.text)
|
|
982
|
+
.join('\n');
|
|
983
|
+
}
|
|
984
|
+
else if (result != null) {
|
|
985
|
+
text = String(result);
|
|
986
|
+
}
|
|
987
|
+
const paths = [...text.matchAll(/^MEDIA:\s*(\S+)/gm)].map(m => m[1].trim());
|
|
988
|
+
if (paths.length === 0)
|
|
989
|
+
return;
|
|
990
|
+
const sessionKey = ctx.sessionKey;
|
|
991
|
+
if (!sessionKey)
|
|
992
|
+
return;
|
|
993
|
+
const existing = mediaStash.get(sessionKey) ?? [];
|
|
994
|
+
mediaStash.set(sessionKey, [...existing, ...paths]);
|
|
995
|
+
console.log(`[clawchats] media-capture: stashed ${paths.length} path(s) for ${sessionKey}:`, paths);
|
|
996
|
+
}, { name: 'clawchats-media-capture', description: 'Captures MEDIA: paths from exec results for inline image rendering' });
|
|
961
997
|
// Background service: signaling + gateway bridge + future WebRTC
|
|
962
998
|
api.registerService({
|
|
963
999
|
id: 'connector-service',
|
|
964
|
-
start: (ctx) => startClawChats(ctx, api),
|
|
1000
|
+
start: (ctx) => startClawChats(ctx, api, mediaStash),
|
|
965
1001
|
stop: (ctx) => stopClawChats(ctx),
|
|
966
1002
|
});
|
|
967
1003
|
// CLI commands
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -3969,7 +3969,6 @@ export function createApp(config = {}) {
|
|
|
3969
3969
|
this._externalBroadcastTargets = [];
|
|
3970
3970
|
this.streamState = new Map();
|
|
3971
3971
|
this.activityLogs = new Map();
|
|
3972
|
-
this._pendingMediaUrls = new Map(); // sessionKey → string[] of MEDIA: paths captured during streaming
|
|
3973
3972
|
setInterval(() => {
|
|
3974
3973
|
const cutoff = Date.now() - 10 * 60 * 1000;
|
|
3975
3974
|
for (const [runId, log] of this.activityLogs) {
|
|
@@ -4062,46 +4061,21 @@ export function createApp(config = {}) {
|
|
|
4062
4061
|
const db = _getDb(parsed.workspace);
|
|
4063
4062
|
const thread = db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId);
|
|
4064
4063
|
if (!thread) { console.log(`Ignoring response for deleted thread: ${parsed.threadId}`); return; }
|
|
4065
|
-
|
|
4064
|
+
let content = extractContent(message);
|
|
4066
4065
|
if (!content || !content.trim()) { console.log(`Skipping empty assistant response for thread ${parsed.threadId}`); return; }
|
|
4067
|
-
const now = Date.now();
|
|
4068
4066
|
|
|
4069
|
-
//
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
console.log(
|
|
4078
|
-
attachments = mediaMatches.map(filePath => {
|
|
4079
|
-
const name = path.basename(filePath);
|
|
4080
|
-
const ext = (name.split('.').pop() || '').toLowerCase();
|
|
4081
|
-
const type = ['jpg','jpeg','png','gif','webp','svg','bmp','ico'].includes(ext) ? 'image'
|
|
4082
|
-
: ['mp3','wav','ogg','aac','m4a','flac','opus'].includes(ext) ? 'audio'
|
|
4083
|
-
: ['mp4','webm','mov','avi','mkv'].includes(ext) ? 'video' : 'file';
|
|
4084
|
-
return { path: filePath, name, type };
|
|
4085
|
-
});
|
|
4067
|
+
// Attach any images captured by the after_tool_call hook (MEDIA: lines from exec stdout).
|
|
4068
|
+
// Always clear the stash entry regardless of whether we find paths, to prevent leaking across turns.
|
|
4069
|
+
const _mediaStash = config.mediaStash;
|
|
4070
|
+
const _pendingPaths = _mediaStash?.get(sessionKey) ?? [];
|
|
4071
|
+
_mediaStash?.delete(sessionKey);
|
|
4072
|
+
if (_pendingPaths.length > 0) {
|
|
4073
|
+
const imageMarkdown = _pendingPaths.map(p => ``).join('\n');
|
|
4074
|
+
content = content.trimEnd() + '\n\n' + imageMarkdown;
|
|
4075
|
+
console.log(`[clawchats] media-attach: appended ${_pendingPaths.length} image(s) to message for ${sessionKey}`);
|
|
4086
4076
|
}
|
|
4087
4077
|
|
|
4088
|
-
|
|
4089
|
-
if (!attachments) {
|
|
4090
|
-
const paths = this._pendingMediaUrls.get(sessionKey);
|
|
4091
|
-
this._pendingMediaUrls.delete(sessionKey);
|
|
4092
|
-
if (paths?.length) {
|
|
4093
|
-
attachments = paths.map(filePath => {
|
|
4094
|
-
const name = path.basename(filePath);
|
|
4095
|
-
const ext = (name.split('.').pop() || '').toLowerCase();
|
|
4096
|
-
const type = ['jpg','jpeg','png','gif','webp','svg','bmp','ico'].includes(ext) ? 'image'
|
|
4097
|
-
: ['mp3','wav','ogg','aac','m4a','flac','opus'].includes(ext) ? 'audio'
|
|
4098
|
-
: ['mp4','webm','mov','avi','mkv'].includes(ext) ? 'video' : 'file';
|
|
4099
|
-
return { path: filePath, name, type };
|
|
4100
|
-
});
|
|
4101
|
-
}
|
|
4102
|
-
} else {
|
|
4103
|
-
this._pendingMediaUrls.delete(sessionKey); // clean up either way
|
|
4104
|
-
}
|
|
4078
|
+
const now = Date.now();
|
|
4105
4079
|
|
|
4106
4080
|
// Check for pending activity message
|
|
4107
4081
|
const pendingMsg = db.prepare(`
|
|
@@ -4122,15 +4096,13 @@ export function createApp(config = {}) {
|
|
|
4122
4096
|
if (lastAssistantIdx >= 0) metadata.activityLog.splice(lastAssistantIdx, 1);
|
|
4123
4097
|
metadata.activitySummary = this.generateActivitySummary(metadata.activityLog);
|
|
4124
4098
|
}
|
|
4125
|
-
if (attachments) metadata.attachments = attachments;
|
|
4126
4099
|
db.prepare('UPDATE messages SET content = ?, metadata = ?, timestamp = ? WHERE id = ?')
|
|
4127
4100
|
.run(content, JSON.stringify(metadata), now, pendingMsg.id);
|
|
4128
4101
|
messageId = pendingMsg.id;
|
|
4129
4102
|
} else {
|
|
4130
4103
|
// No pending activity — normal INSERT (simple responses, no tools)
|
|
4131
4104
|
messageId = seq != null ? `gw-${parsed.threadId}-${seq}` : `gw-${parsed.threadId}-${now}`;
|
|
4132
|
-
|
|
4133
|
-
db.prepare(`INSERT INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at) VALUES (?, ?, 'assistant', ?, 'sent', ?, ?, ?) ON CONFLICT(id) DO UPDATE SET content = excluded.content, metadata = COALESCE(excluded.metadata, metadata), timestamp = excluded.timestamp`).run(messageId, parsed.threadId, content, metaStr, now, now);
|
|
4105
|
+
db.prepare(`INSERT INTO messages (id, thread_id, role, content, status, timestamp, created_at) VALUES (?, ?, 'assistant', ?, 'sent', ?, ?) ON CONFLICT(id) DO UPDATE SET content = excluded.content, timestamp = excluded.timestamp`).run(messageId, parsed.threadId, content, now, now);
|
|
4134
4106
|
}
|
|
4135
4107
|
|
|
4136
4108
|
try {
|
|
@@ -4172,8 +4144,6 @@ export function createApp(config = {}) {
|
|
|
4172
4144
|
} catch (e) { console.error(`Failed to save error marker:`, e.message); }
|
|
4173
4145
|
}
|
|
4174
4146
|
|
|
4175
|
-
// _scanRecentWorkspaceFiles removed — MEDIA: paths captured from delta stream instead
|
|
4176
|
-
|
|
4177
4147
|
generateThreadTitle(db, threadId, workspace, skipHeuristic = false) {
|
|
4178
4148
|
const thread = db.prepare('SELECT title FROM threads WHERE id = ?').get(threadId);
|
|
4179
4149
|
if (!thread) return;
|
|
@@ -4224,25 +4194,7 @@ export function createApp(config = {}) {
|
|
|
4224
4194
|
if (!this.activityLogs.has(runId)) this.activityLogs.set(runId, { sessionKey, steps: [], startTime: Date.now() });
|
|
4225
4195
|
const log = this.activityLogs.get(runId);
|
|
4226
4196
|
if (stream === 'assistant') {
|
|
4227
|
-
// Check data.mediaUrls — gateway populates this via splitMediaFromOutput on each streaming event.
|
|
4228
|
-
// Partial path events (e.g. ["/"]) fail the extension filter; complete-path events pass.
|
|
4229
|
-
const incomingUrls = Array.isArray(data?.mediaUrls) ? data.mediaUrls : (data?.mediaUrl ? [data.mediaUrl] : []);
|
|
4230
|
-
for (const p of incomingUrls) {
|
|
4231
|
-
if (typeof p === 'string' && /\.\w{1,10}$/.test(p)) {
|
|
4232
|
-
if (!log._seenMediaPaths) log._seenMediaPaths = new Set();
|
|
4233
|
-
if (!log._seenMediaPaths.has(p)) {
|
|
4234
|
-
log._seenMediaPaths.add(p);
|
|
4235
|
-
if (!this._pendingMediaUrls.has(sessionKey)) this._pendingMediaUrls.set(sessionKey, []);
|
|
4236
|
-
this._pendingMediaUrls.get(sessionKey).push(p);
|
|
4237
|
-
console.log('[ClawChats] MEDIA path captured from mediaUrls:', p);
|
|
4238
|
-
}
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
|
-
if (incomingUrls.length > 0) console.log('[ClawChats] mediaUrls on event:', JSON.stringify(incomingUrls));
|
|
4242
4197
|
const text = data?.text || '';
|
|
4243
|
-
const delta = data?.delta || '';
|
|
4244
|
-
// Log if a MEDIA: directive appears in text or delta (must look like an actual path, not just the word)
|
|
4245
|
-
if (/MEDIA:\s*[./~a-zA-Z]/.test(text.slice(-80)) || /MEDIA:\s*[./~a-zA-Z]/.test(delta)) console.log('[ClawChats] MEDIA in stream! text tail:', JSON.stringify(text.slice(-80)), 'delta:', JSON.stringify(delta));
|
|
4246
4198
|
if (text) {
|
|
4247
4199
|
let currentSegment = log._currentAssistantSegment;
|
|
4248
4200
|
if (!currentSegment || currentSegment._sealed) {
|
|
@@ -4269,7 +4221,6 @@ export function createApp(config = {}) {
|
|
|
4269
4221
|
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) { log._currentAssistantSegment._sealed = true; }
|
|
4270
4222
|
const step = { type: 'tool', timestamp: Date.now(), name: data?.name || 'unknown', phase: data?.phase || 'start', toolCallId: data?.toolCallId, meta: data?.meta, isError: data?.isError || false };
|
|
4271
4223
|
if (data?.phase === 'result') {
|
|
4272
|
-
if (data?.name === 'exec') console.log('[ClawChats] exec tool result meta:', JSON.stringify(data?.meta));
|
|
4273
4224
|
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId && (s.phase === 'start' || s.phase === 'running'));
|
|
4274
4225
|
if (existing) { existing.phase = 'done'; existing.resultMeta = data?.meta; existing.isError = data?.isError || false; existing.durationMs = Date.now() - existing.timestamp; }
|
|
4275
4226
|
else { step.phase = 'done'; log.steps.push(step); }
|
|
@@ -4282,7 +4233,6 @@ export function createApp(config = {}) {
|
|
|
4282
4233
|
}
|
|
4283
4234
|
if (stream === 'lifecycle') {
|
|
4284
4235
|
if (data?.phase === 'end' || data?.phase === 'error') {
|
|
4285
|
-
// MEDIA: paths already captured during delta streaming — nothing to do here
|
|
4286
4236
|
if (log._currentAssistantSegment && !log._currentAssistantSegment._sealed) log._currentAssistantSegment._sealed = true;
|
|
4287
4237
|
const lastAssistantIdx = log.steps.findLastIndex(s => s.type === 'assistant');
|
|
4288
4238
|
if (lastAssistantIdx >= 0) log.steps.splice(lastAssistantIdx, 1);
|
|
@@ -4609,7 +4559,7 @@ export function createApp(config = {}) {
|
|
|
4609
4559
|
}
|
|
4610
4560
|
|
|
4611
4561
|
// ── Browser WebSocket setup (shared logic for standalone and plugin) ────────
|
|
4612
|
-
|
|
4562
|
+
function _setupBrowserWs(wssInstance) {
|
|
4613
4563
|
wssInstance.on('connection', (ws) => {
|
|
4614
4564
|
console.log('Browser client connected');
|
|
4615
4565
|
_gatewayClient.addBrowserClient(ws);
|
|
@@ -4619,7 +4569,6 @@ export function createApp(config = {}) {
|
|
|
4619
4569
|
ws.on('message', (data) => {
|
|
4620
4570
|
const msgStr = data.toString();
|
|
4621
4571
|
_debugLogger.logFrame('BR→SRV', msgStr);
|
|
4622
|
-
let forwardStr = msgStr;
|
|
4623
4572
|
try {
|
|
4624
4573
|
const msg = JSON.parse(msgStr);
|
|
4625
4574
|
if (msg.type === 'req' && msg.method === 'connect') {
|
|
@@ -4639,9 +4588,8 @@ export function createApp(config = {}) {
|
|
|
4639
4588
|
if (msg.action === 'debug-start') { const result = _debugLogger.start(msg.ts, ws); if (result.error === 'already-active') ws.send(JSON.stringify({ type: 'clawchats', event: 'debug-error', error: 'Recording already active in another tab', sessionId: result.sessionId })); else ws.send(JSON.stringify({ type: 'clawchats', event: 'debug-started', sessionId: result.sessionId })); return; }
|
|
4640
4589
|
if (msg.action === 'debug-dump') { const { sessionId, files } = _debugLogger.saveDump(msg); ws.send(JSON.stringify({ type: 'clawchats', event: 'debug-saved', sessionId, files })); return; }
|
|
4641
4590
|
}
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
_gatewayClient.sendToGateway(forwardStr);
|
|
4591
|
+
} catch { /* Not JSON or not a ClawChats message, forward to gateway */ }
|
|
4592
|
+
_gatewayClient.sendToGateway(msgStr);
|
|
4645
4593
|
});
|
|
4646
4594
|
|
|
4647
4595
|
ws.on('close', () => { console.log('Browser client disconnected'); _debugLogger.handleClientDisconnect(ws); _gatewayClient.removeBrowserClient(ws); });
|
|
@@ -4697,7 +4645,7 @@ if (isDirectRun) {
|
|
|
4697
4645
|
app.gatewayClient.connect();
|
|
4698
4646
|
|
|
4699
4647
|
// Initialize global DB (custom emojis, etc.)
|
|
4700
|
-
getGlobalDb(
|
|
4648
|
+
getGlobalDb(DATA_DIR);
|
|
4701
4649
|
});
|
|
4702
4650
|
|
|
4703
4651
|
// Graceful shutdown
|