@clawchatsai/connector 0.1.6 → 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 +6 -34
- package/package.json +1 -1
- package/server/gateway.js +45 -6
- package/server/index.js +1 -2
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
|
|
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
|
|
1301
|
+
start: (ctx) => startClawChats(ctx, api),
|
|
1330
1302
|
stop: (ctx) => stopClawChats(ctx),
|
|
1331
1303
|
});
|
|
1332
1304
|
// CLI commands
|
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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;
|
|
@@ -22,6 +24,10 @@ export class GatewayClient {
|
|
|
22
24
|
this.streamState = new Map();
|
|
23
25
|
this.activityLogs = new Map();
|
|
24
26
|
this._pendingTitleGens = new Map();
|
|
27
|
+
// Runs we've already synthesized a streaming-end{reason:'error'} for
|
|
28
|
+
// (agent lifecycle:error emitted but no chat state:error ever will). Prevents
|
|
29
|
+
// double-fires across retries and cross-path dedup with handleChatEvent.
|
|
30
|
+
this._syntheticErrorRuns = new Set();
|
|
25
31
|
|
|
26
32
|
// On startup: mark any pending activity log messages from previous crashed/restarted sessions
|
|
27
33
|
// as interrupted. Without this, stale pending=true rows survive gateway restarts and cause
|
|
@@ -224,10 +230,10 @@ export class GatewayClient {
|
|
|
224
230
|
// Intermediate narration lives in activityLog steps; message.content is the clean final answer.
|
|
225
231
|
let content = sanitizeAssistantContent(extractContent(message).substring(thoughtStartOffset));
|
|
226
232
|
|
|
227
|
-
// Attach media (MEDIA:
|
|
228
|
-
//
|
|
229
|
-
const pendingPaths = this.
|
|
230
|
-
this.
|
|
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);
|
|
231
237
|
const IMAGE_EXTS = new Set(['png','jpg','jpeg','gif','webp','bmp','svg','ico','avif','tiff']);
|
|
232
238
|
const AUDIO_EXTS = new Set(['mp3','wav','ogg','m4a','flac','aac','opus','wma']);
|
|
233
239
|
const imagePaths = [], pendingAttachments = [];
|
|
@@ -363,6 +369,16 @@ export class GatewayClient {
|
|
|
363
369
|
log._currentAssistantSegment._sealed = true;
|
|
364
370
|
}
|
|
365
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
|
+
}
|
|
366
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 };
|
|
367
383
|
if (data?.phase === 'result') {
|
|
368
384
|
const existing = log.steps.findLast(s => s.toolCallId === data.toolCallId && (s.phase === 'start' || s.phase === 'running'));
|
|
@@ -402,6 +418,29 @@ export class GatewayClient {
|
|
|
402
418
|
log.finalSteps = cleanSteps;
|
|
403
419
|
log.finalSummary = generateActivitySummary(log.steps);
|
|
404
420
|
// Note: activityLogs entry is kept until _popActivityLogForSession cleans it up
|
|
421
|
+
//
|
|
422
|
+
// Fallback: if the gateway never emitted any chat.* events for this session
|
|
423
|
+
// (e.g. pre-reply provider error on a non-webchat surface where the chat
|
|
424
|
+
// broadcast is gated off upstream), handleChatEvent won't fire a
|
|
425
|
+
// streaming-end and the UI stays locked on "thinking…" forever. Synthesize
|
|
426
|
+
// one here so the frontend's existing error path (`reason: 'error'`) runs.
|
|
427
|
+
if (data?.phase === 'error' && !this.streamState.has(sessionKey) && !this._syntheticErrorRuns.has(runId)) {
|
|
428
|
+
this._syntheticErrorRuns.add(runId);
|
|
429
|
+
const parsed = parseSessionKey(sessionKey);
|
|
430
|
+
if (parsed) {
|
|
431
|
+
this.broadcastToBrowsers(JSON.stringify({
|
|
432
|
+
type: 'clawchats',
|
|
433
|
+
event: 'streaming-end',
|
|
434
|
+
threadId: parsed.threadId,
|
|
435
|
+
workspace: parsed.workspace,
|
|
436
|
+
reason: 'error',
|
|
437
|
+
errorMessage: data?.error || 'Agent failed before reply',
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
// Bound the set — auto-evict after the retry window so long-lived
|
|
441
|
+
// processes don't leak. 5 min is far longer than any retry chain.
|
|
442
|
+
setTimeout(() => this._syntheticErrorRuns.delete(runId), 5 * 60 * 1000);
|
|
443
|
+
}
|
|
405
444
|
}
|
|
406
445
|
}
|
|
407
446
|
|
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
|
|
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
|