@clawchatsai/connector 0.1.7 → 0.1.8

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 CHANGED
@@ -144,7 +144,7 @@ Notes:
144
144
  - Works for images (png, jpg, gif, webp, svg, etc.), documents, markdown, code files, and more
145
145
  - Output each path once — duplicates are ignored automatically
146
146
  `;
147
- async function startClawChats(ctx, api, mediaStash) {
147
+ async function startClawChats(ctx, api) {
148
148
  _stopRequested = false;
149
149
  // Bootstrap CLAWCHATS.md in the agent workspace so the agent always knows the MEDIA: protocol.
150
150
  // Written once on plugin start; never overwrites an existing file (user may have customised it).
@@ -255,7 +255,6 @@ async function startClawChats(ctx, api, mediaStash) {
255
255
  return process.env.OPENAI_API_KEY || null;
256
256
  })(),
257
257
  memoryEnv: memoryEnvFiltered,
258
- mediaStash, // Shared Map populated by after_tool_call hook (captures MEDIA: paths from exec)
259
258
  });
260
259
  // 4. Connect createApp's gateway client (handles persistence + event relay)
261
260
  app.gatewayClient.connect();
@@ -1284,49 +1283,22 @@ const plugin = {
1284
1283
  name: 'ClawChats',
1285
1284
  description: 'Connects your gateway to ClawChats via WebRTC P2P',
1286
1285
  register(api) {
1287
- // Shared stash: after_tool_call hook populates this; saveAssistantMessage reads + clears it.
1288
- // Keyed by sessionKey → absolute paths extracted from MEDIA: lines in exec stdout.
1289
- const mediaStash = new Map();
1290
1286
  // Inject MEDIA: capability note into the system prompt via before_prompt_build.
1291
1287
  // Appended to system context (trusted, not user-turn) so it's always present and
1292
1288
  // never flagged as prompt injection. Survives compaction automatically.
1289
+ //
1290
+ // Path parsing lives in server/gateway.js handleAgentEvent (stable singleton) —
1291
+ // do NOT hold state in this closure; OpenClaw may call register() multiple times
1292
+ // during the plugin lifecycle and each call creates a fresh closure.
1293
1293
  api.on('before_prompt_build', (_event, _ctx) => {
1294
1294
  return {
1295
1295
  appendSystemContext: 'ClawChats inline preview: after writing a file with the Write tool, run `echo "MEDIA:/absolute/path/to/file"` via the exec tool to display it inline in the chat UI.',
1296
1296
  };
1297
1297
  }, { name: 'clawchats-media-hint', description: 'Appends MEDIA: file preview capability note to system prompt' });
1298
- api.on('after_tool_call', (event, ctx) => {
1299
- if (event.toolName !== 'exec' && event.toolName !== 'process')
1300
- return;
1301
- // result is { content: [{ type: 'text', text: '...' }] } — extract text defensively
1302
- const result = event.result;
1303
- let text = '';
1304
- if (typeof result?.text === 'string') {
1305
- text = result.text;
1306
- }
1307
- else if (Array.isArray(result?.content)) {
1308
- text = result.content
1309
- .filter(c => c.type === 'text' && typeof c.text === 'string')
1310
- .map(c => c.text)
1311
- .join('\n');
1312
- }
1313
- else if (result != null) {
1314
- text = String(result);
1315
- }
1316
- const paths = [...text.matchAll(/^MEDIA:\s*(\S+)/gm)].map(m => m[1].trim());
1317
- if (paths.length === 0)
1318
- return;
1319
- const sessionKey = ctx.sessionKey;
1320
- if (!sessionKey)
1321
- return;
1322
- const existing = mediaStash.get(sessionKey) ?? [];
1323
- mediaStash.set(sessionKey, [...new Set([...existing, ...paths])]);
1324
- console.log(`[clawchats] media-capture: stashed ${paths.length} path(s) for ${sessionKey}:`, paths);
1325
- }, { name: 'clawchats-media-capture', description: 'Captures MEDIA: paths from exec results for inline image rendering' });
1326
1298
  // Background service: signaling + gateway bridge + future WebRTC
1327
1299
  api.registerService({
1328
1300
  id: 'connector-service',
1329
- start: (ctx) => startClawChats(ctx, api, mediaStash),
1301
+ start: (ctx) => startClawChats(ctx, api),
1330
1302
  stop: (ctx) => stopClawChats(ctx),
1331
1303
  });
1332
1304
  // CLI commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "ClawChats OpenClaw plugin — P2P tunnel + local API bridge",
6
6
  "main": "dist/index.js",
package/server/gateway.js CHANGED
@@ -4,14 +4,16 @@ import { loadOrCreateDeviceIdentity, buildDeviceAuth } from './bootstrap/identit
4
4
  import { parseSessionKey, extractContent, isSilentReplyExact, isSilentReplyPrefix, sanitizeAssistantContent, syncThreadUnreadCount, generateActivitySummary, writeActivityToDb } from './util/helpers.js';
5
5
 
6
6
  export class GatewayClient {
7
- constructor({ getDb, getWorkspaces, dataDir, debugLogger, gatewayWsUrl, authToken, mediaStash }) {
7
+ constructor({ getDb, getWorkspaces, dataDir, debugLogger, gatewayWsUrl, authToken }) {
8
8
  this.getDb = getDb;
9
9
  this.getWorkspaces = getWorkspaces;
10
10
  this.dataDir = dataDir;
11
11
  this.debugLogger = debugLogger;
12
12
  this.gatewayWsUrl = gatewayWsUrl;
13
13
  this.authToken = authToken;
14
- this.mediaStash = mediaStash;
14
+ // Per-session buffer of MEDIA: paths extracted from exec tool args (echo "MEDIA:/...").
15
+ // Lives on the stable singleton — safe from plugin re-registration that broke the old closure-based stash.
16
+ this._runMediaPaths = new Map();
15
17
 
16
18
  this.ws = null;
17
19
  this.connected = false;
@@ -228,10 +230,10 @@ export class GatewayClient {
228
230
  // Intermediate narration lives in activityLog steps; message.content is the clean final answer.
229
231
  let content = sanitizeAssistantContent(extractContent(message).substring(thoughtStartOffset));
230
232
 
231
- // Attach media (MEDIA: lines from exec stdout captured by after_tool_call hook).
232
- // Stash is read before the empty-content guard — media-only responses (no text) must not be dropped.
233
- const pendingPaths = this.mediaStash?.get(sessionKey) ?? [];
234
- this.mediaStash?.delete(sessionKey);
233
+ // Attach media (MEDIA: paths extracted from exec tool args by handleAgentEvent).
234
+ // Buffer is read before the empty-content guard — media-only responses (no text) must not be dropped.
235
+ const pendingPaths = this._runMediaPaths.get(sessionKey) ?? [];
236
+ this._runMediaPaths.delete(sessionKey);
235
237
  const IMAGE_EXTS = new Set(['png','jpg','jpeg','gif','webp','bmp','svg','ico','avif','tiff']);
236
238
  const AUDIO_EXTS = new Set(['mp3','wav','ogg','m4a','flac','aac','opus','wma']);
237
239
  const imagePaths = [], pendingAttachments = [];
@@ -367,6 +369,16 @@ export class GatewayClient {
367
369
  log._currentAssistantSegment._sealed = true;
368
370
  }
369
371
  const argsMeta = data?.args ? (data.args.command || data.args.path || data.args.query || data.args.url || Object.values(data.args).find(v => typeof v === 'string') || '') : '';
372
+ // Extract MEDIA: paths from exec tool's echo command at phase:start.
373
+ // The agent signals inline media with `echo "MEDIA:/absolute/path"` (see plugin's before_prompt_build hint).
374
+ // Parsing the tool args here keeps state on this stable singleton instead of the plugin's re-registered closure.
375
+ if (sessionKey && (data?.name === 'exec' || data?.name === 'process') && data?.phase === 'start' && typeof data?.args?.command === 'string') {
376
+ const paths = [...data.args.command.matchAll(/MEDIA:([^\s"'`]+)/g)].map(m => m[1]).filter(Boolean);
377
+ if (paths.length > 0) {
378
+ const existing = this._runMediaPaths.get(sessionKey) ?? [];
379
+ this._runMediaPaths.set(sessionKey, [...new Set([...existing, ...paths])]);
380
+ }
381
+ }
370
382
  const step = { type: 'tool', timestamp: Date.now(), name: data?.name || 'unknown', phase: data?.phase || 'start', toolCallId: data?.toolCallId, meta: data?.meta || (argsMeta ? String(argsMeta) : undefined), isError: data?.isError || false };
371
383
  if (data?.phase === 'result') {
372
384
  const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId && (s.phase === 'start' || s.phase === 'running'));
package/server/index.js CHANGED
@@ -83,7 +83,6 @@ export function createApp(config = {}) {
83
83
  const { getWorkspaces, setWorkspaces } = createWorkspaceStore(WORKSPACES_FILE);
84
84
 
85
85
  const debugLogger = new DebugLogger(DATA_DIR);
86
- const mediaStash = config.mediaStash ?? new Map();
87
86
 
88
87
  const memoryConfig = discoverMemoryConfig(config.memoryEnv || {});
89
88
  const memoryProvider = createMemoryProvider(memoryConfig);
@@ -91,7 +90,7 @@ export function createApp(config = {}) {
91
90
  const MEMORY_FILES_DIR = path.join(memoryConfig.workspaceDir, 'memory');
92
91
 
93
92
  // Instantiate the gateway client with all dependencies injected
94
- const gatewayClient = new GatewayClient({ getDb, getWorkspaces, dataDir: DATA_DIR, debugLogger, gatewayWsUrl: gatewayUrl, authToken: gatewayToken, mediaStash });
93
+ const gatewayClient = new GatewayClient({ getDb, getWorkspaces, dataDir: DATA_DIR, debugLogger, gatewayWsUrl: gatewayUrl, authToken: gatewayToken });
95
94
  const broadcast = msg => gatewayClient.broadcastToBrowsers(msg);
96
95
 
97
96
  // Instantiate controllers