@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
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
@@ -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
- const content = extractContent(message);
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
- // Check if the final assembled message content contains MEDIA: lines
4070
- console.log('[ClawChats] saveAssistantMessage rawContent:', JSON.stringify(content.slice(0, 300)), 'rawMsgContent:', JSON.stringify(JSON.stringify(message?.content)?.slice(0, 300)));
4071
- console.log('[ClawChats] saveAssistantMessage msgKeys:', Object.keys(message||{}), 'msgMediaUrls:', JSON.stringify(message?.mediaUrls), 'msgMediaUrl:', message?.mediaUrl);
4072
-
4073
- // Extract MEDIA: paths from the final complete message content (if gateway preserves them here)
4074
- let attachments = null;
4075
- const mediaMatches = [...content.matchAll(/^MEDIA:\s*(\S+)/gm)].map(m => m[1].trim()).filter(p => /\.\w{1,10}$/.test(p));
4076
- if (mediaMatches.length > 0) {
4077
- console.log('[ClawChats] MEDIA paths from final content:', JSON.stringify(mediaMatches));
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 => `![image](${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
- // Fallback: use MEDIA: paths captured from delta stream
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
- const metaStr = attachments ? JSON.stringify({ attachments }) : null;
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
- function _setupBrowserWs(wssInstance) {
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
- // (no hint injection mediaUrls are captured from agent events directly)
4643
- } catch { /* Not JSON or not a ClawChats message, forward as-is */ }
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(_DATA_DIR);
4648
+ getGlobalDb(DATA_DIR);
4701
4649
  });
4702
4650
 
4703
4651
  // Graceful shutdown