@clawchatsai/connector 0.1.7 → 0.1.9
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 +24 -7
- 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;
|
|
@@ -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:
|
|
232
|
-
//
|
|
233
|
-
const pendingPaths = this.
|
|
234
|
-
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);
|
|
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 = [];
|
|
@@ -290,7 +292,7 @@ export class GatewayClient {
|
|
|
290
292
|
if (!db.prepare('SELECT id FROM threads WHERE id = ?').get(parsed.threadId)) return;
|
|
291
293
|
const now = Date.now();
|
|
292
294
|
try {
|
|
293
|
-
db.prepare('INSERT INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(`gw-error-${parsed.threadId}-${now}`, parsed.threadId, 'system', `[error] ${
|
|
295
|
+
db.prepare('INSERT INTO messages (id, thread_id, role, content, status, metadata, timestamp, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(`gw-error-${parsed.threadId}-${now}`, parsed.threadId, 'system', `[error] ${message?.error || message?.content || 'Unknown error'}`, 'sent', '{"transient":true}', now, now);
|
|
294
296
|
// Clear stale pending flag so browsers reloading the chat don't re-derive "thinking..." state.
|
|
295
297
|
db.prepare("UPDATE messages SET metadata = json_remove(metadata, '$.pending') WHERE thread_id = ? AND role = 'assistant' AND json_extract(metadata, '$.pending') = 1").run(parsed.threadId);
|
|
296
298
|
} catch (e) { console.error('Failed to save error marker:', e.message); }
|
|
@@ -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'));
|
|
@@ -416,6 +428,11 @@ export class GatewayClient {
|
|
|
416
428
|
this._syntheticErrorRuns.add(runId);
|
|
417
429
|
const parsed = parseSessionKey(sessionKey);
|
|
418
430
|
if (parsed) {
|
|
431
|
+
// Mirror the state:'error' path — writeActivityToDb just set metadata.pending=true,
|
|
432
|
+
// and without this the flag survives: loadHistory re-derives has_pending on reconnect,
|
|
433
|
+
// "thinking…" sticks, and sendMessage's isStreaming() guard queues future sends into
|
|
434
|
+
// _pendingSend forever.
|
|
435
|
+
this.saveErrorMarker(sessionKey, { error: data?.error || 'Agent failed before reply' });
|
|
419
436
|
this.broadcastToBrowsers(JSON.stringify({
|
|
420
437
|
type: 'clawchats',
|
|
421
438
|
event: 'streaming-end',
|
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
|