@clawchatsai/connector 0.0.69 → 0.0.70

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +138 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.69",
3
+ "version": "0.0.70",
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
@@ -277,6 +277,21 @@ const OPENCLAW_SESSIONS_DIR = (() => {
277
277
  return fallback;
278
278
  })();
279
279
 
280
+ // ─── Agent Helpers ───────────────────────────────────────────────────────────
281
+
282
+ function getSessionsDirForAgent(agentId) {
283
+ if (!agentId || agentId === 'main') return OPENCLAW_SESSIONS_DIR;
284
+ return path.join(HOME, '.openclaw', 'agents', agentId, 'sessions');
285
+ }
286
+
287
+ function validateAgent(agentId) {
288
+ if (!agentId) return 'main';
289
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) throw new Error('Invalid agent ID');
290
+ const agentDir = path.join(HOME, '.openclaw', 'agents', agentId);
291
+ if (!fs.existsSync(agentDir)) throw new Error(`Agent not found: ${agentId}`);
292
+ return agentId;
293
+ }
294
+
280
295
  // ─── Workspace Management ───────────────────────────────────────────────────
281
296
 
282
297
  function ensureDirs() {
@@ -619,13 +634,15 @@ function parseMultipart(req) {
619
634
 
620
635
  // ─── Context Fill Helper ────────────────────────────────────────────────────
621
636
 
622
- function buildContextPreamble(db, threadId, lastSessionId) {
637
+ function buildContextPreamble(db, threadId, lastSessionId, sessionKey) {
623
638
  let summary = null;
624
639
  let method = 'raw';
625
640
 
626
641
  // Try to read old JSONL transcript
627
642
  if (lastSessionId) {
628
- const jsonlPath = path.join(OPENCLAW_SESSIONS_DIR, `${lastSessionId}.jsonl`);
643
+ const agentMatch = (sessionKey || '').match(/^agent:([^:]+):/);
644
+ const sessionsDir = getSessionsDirForAgent(agentMatch?.[1]);
645
+ const jsonlPath = path.join(sessionsDir, `${lastSessionId}.jsonl`);
629
646
  try {
630
647
  const content = fs.readFileSync(jsonlPath, 'utf8');
631
648
  const lines = content.split('\n').filter(Boolean);
@@ -696,7 +713,9 @@ function buildContextPreamble(db, threadId, lastSessionId) {
696
713
 
697
714
  function cleanGatewaySession(sessionKey) {
698
715
  try {
699
- const sessionsPath = path.join(OPENCLAW_SESSIONS_DIR, 'sessions.json');
716
+ const agentMatch = (sessionKey || '').match(/^agent:([^:]+):/);
717
+ const sessionsDir = getSessionsDirForAgent(agentMatch?.[1]);
718
+ const sessionsPath = path.join(sessionsDir, 'sessions.json');
700
719
  const raw = fs.readFileSync(sessionsPath, 'utf8');
701
720
  const store = JSON.parse(raw);
702
721
  const entry = store[sessionKey];
@@ -704,7 +723,7 @@ function cleanGatewaySession(sessionKey) {
704
723
 
705
724
  // Delete .jsonl transcript
706
725
  if (entry.sessionId) {
707
- const jsonlPath = path.join(OPENCLAW_SESSIONS_DIR, `${entry.sessionId}.jsonl`);
726
+ const jsonlPath = path.join(sessionsDir, `${entry.sessionId}.jsonl`);
708
727
  try { fs.unlinkSync(jsonlPath); } catch { /* ok */ }
709
728
  }
710
729
 
@@ -721,7 +740,9 @@ function cleanGatewaySession(sessionKey) {
721
740
 
722
741
  function cleanGatewaySessionsByPrefix(prefix) {
723
742
  try {
724
- const sessionsPath = path.join(OPENCLAW_SESSIONS_DIR, 'sessions.json');
743
+ const agentMatch = (prefix || '').match(/^agent:([^:]+):/);
744
+ const sessionsDir = getSessionsDirForAgent(agentMatch?.[1]);
745
+ const sessionsPath = path.join(sessionsDir, 'sessions.json');
725
746
  const raw = fs.readFileSync(sessionsPath, 'utf8');
726
747
  const store = JSON.parse(raw);
727
748
  let cleaned = 0;
@@ -730,7 +751,7 @@ function cleanGatewaySessionsByPrefix(prefix) {
730
751
  if (!key.startsWith(prefix)) continue;
731
752
  const entry = store[key];
732
753
  if (entry?.sessionId) {
733
- const jsonlPath = path.join(OPENCLAW_SESSIONS_DIR, `${entry.sessionId}.jsonl`);
754
+ const jsonlPath = path.join(sessionsDir, `${entry.sessionId}.jsonl`);
734
755
  try { fs.unlinkSync(jsonlPath); } catch { /* ok */ }
735
756
  }
736
757
  delete store[key];
@@ -811,7 +832,9 @@ async function handleCreateWorkspace(req, res) {
811
832
  if (ws.workspaces[name]) {
812
833
  return sendError(res, 409, 'Workspace already exists');
813
834
  }
814
- const workspace = { name, label: label || name, color: body.color || null, icon: body.icon || null, createdAt: Date.now() };
835
+ let agent = 'main';
836
+ try { agent = validateAgent(body.agent || 'main'); } catch { agent = 'main'; }
837
+ const workspace = { name, label: label || name, color: body.color || null, icon: body.icon || null, agent, createdAt: Date.now() };
815
838
  ws.workspaces[name] = workspace;
816
839
  setWorkspaces(ws);
817
840
  // Initialize DB
@@ -837,8 +860,38 @@ async function handleUpdateWorkspace(req, res, params) {
837
860
  if (body.lastThread !== undefined) {
838
861
  ws.workspaces[params.name].lastThread = body.lastThread;
839
862
  }
863
+ let migratedThreads = 0;
864
+ if (body.agent !== undefined) {
865
+ let newAgent;
866
+ try { newAgent = validateAgent(body.agent); } catch (e) { return sendError(res, 400, e.message); }
867
+ const oldAgent = ws.workspaces[params.name].agent || 'main';
868
+ if (newAgent !== oldAgent) {
869
+ const db = getDb(params.name);
870
+ const threads = db.prepare(
871
+ `SELECT id, session_key FROM threads WHERE session_key LIKE ?`
872
+ ).all(`agent:${oldAgent}:${params.name}:chat:%`);
873
+ db.prepare(`
874
+ UPDATE threads
875
+ SET session_key = replace(
876
+ session_key,
877
+ 'agent:' || ? || ':' || ? || ':chat:',
878
+ 'agent:' || ? || ':' || ? || ':chat:'
879
+ )
880
+ WHERE session_key LIKE 'agent:' || ? || ':' || ? || ':chat:%'
881
+ `).run(oldAgent, params.name, newAgent, params.name, oldAgent, params.name);
882
+ for (const t of threads) cleanGatewaySession(t.session_key);
883
+ ws.workspaces[params.name].agent = newAgent;
884
+ migratedThreads = threads.length;
885
+ gatewayClient.broadcastToBrowsers(JSON.stringify({
886
+ type: 'clawchats',
887
+ event: 'workspace-agent-changed',
888
+ workspace: params.name,
889
+ agent: newAgent
890
+ }));
891
+ }
892
+ }
840
893
  setWorkspaces(ws);
841
- send(res, 200, { workspace: ws.workspaces[params.name] });
894
+ send(res, 200, { workspace: ws.workspaces[params.name], migratedThreads });
842
895
  }
843
896
 
844
897
  function handleDeleteWorkspace(req, res, params) {
@@ -861,7 +914,8 @@ function handleDeleteWorkspace(req, res, params) {
861
914
  try { fs.unlinkSync(dbPath + '-shm'); } catch { /* ok */ }
862
915
 
863
916
  // Clean all gateway sessions for this workspace
864
- const cleaned = cleanGatewaySessionsByPrefix(`agent:main:${params.name}:chat:`);
917
+ const wsAgentForDelete = ws.workspaces[params.name]?.agent || 'main';
918
+ const cleaned = cleanGatewaySessionsByPrefix(`agent:${wsAgentForDelete}:${params.name}:chat:`);
865
919
  if (cleaned > 0) {
866
920
  console.log(`Cleaned ${cleaned} gateway sessions for workspace: ${params.name}`);
867
921
  }
@@ -1004,7 +1058,8 @@ async function handleCreateThread(req, res) {
1004
1058
  const ws = getWorkspaces();
1005
1059
  const id = body.id || uuid();
1006
1060
  const now = Date.now();
1007
- const sessionKey = `agent:main:${ws.active}:chat:${id}`;
1061
+ const workspaceAgent = ws.workspaces[ws.active]?.agent || 'main';
1062
+ const sessionKey = `agent:${workspaceAgent}:${ws.active}:chat:${id}`;
1008
1063
 
1009
1064
  try {
1010
1065
  db.prepare(
@@ -1065,9 +1120,11 @@ function handleDeleteThread(req, res, params) {
1065
1120
 
1066
1121
  // Look up sessionId from SQLite or sessions.json as fallback
1067
1122
  let sessionIdToDelete = thread.last_session_id;
1123
+ const threadAgentMatch = (thread.session_key || '').match(/^agent:([^:]+):/);
1124
+ const threadSessionsDir = getSessionsDirForAgent(threadAgentMatch?.[1]);
1068
1125
  if (!sessionIdToDelete) {
1069
1126
  try {
1070
- const raw = fs.readFileSync(path.join(OPENCLAW_SESSIONS_DIR, 'sessions.json'), 'utf8');
1127
+ const raw = fs.readFileSync(path.join(threadSessionsDir, 'sessions.json'), 'utf8');
1071
1128
  const store = JSON.parse(raw);
1072
1129
  const entry = store[thread.session_key];
1073
1130
  if (entry?.sessionId) sessionIdToDelete = entry.sessionId;
@@ -1079,7 +1136,7 @@ function handleDeleteThread(req, res, params) {
1079
1136
 
1080
1137
  // If cleanGatewaySession didn't find it but we have a sessionId, delete transcript directly
1081
1138
  if (sessionIdToDelete) {
1082
- const jsonlPath = path.join(OPENCLAW_SESSIONS_DIR, `${sessionIdToDelete}.jsonl`);
1139
+ const jsonlPath = path.join(threadSessionsDir, `${sessionIdToDelete}.jsonl`);
1083
1140
  try { fs.unlinkSync(jsonlPath); } catch { /* ok */ }
1084
1141
  }
1085
1142
 
@@ -1205,7 +1262,7 @@ function handleContextFill(req, res, params) {
1205
1262
  const thread = db.prepare('SELECT * FROM threads WHERE id = ?').get(params.id);
1206
1263
  if (!thread) return sendError(res, 404, 'Thread not found');
1207
1264
 
1208
- const { preamble, method } = buildContextPreamble(db, params.id, thread.last_session_id);
1265
+ const { preamble, method } = buildContextPreamble(db, params.id, thread.last_session_id, thread.session_key);
1209
1266
  send(res, 200, { preamble, method });
1210
1267
  }
1211
1268
 
@@ -1300,6 +1357,7 @@ async function handleImport(req, res) {
1300
1357
  const importAll = db.transaction(() => {
1301
1358
  for (const t of body.threads) {
1302
1359
  if (!t.id) continue;
1360
+ // TODO: per-project agent — import uses agent:main for now
1303
1361
  const sessionKey = t.session_key || `agent:main:${ws.active}:chat:${t.id}`;
1304
1362
  const result = insertThread.run(
1305
1363
  t.id, sessionKey, t.title || 'Imported chat',
@@ -2409,6 +2467,19 @@ async function _handleRequestImpl(req, res) {
2409
2467
  return await handleTranscribe(req, res);
2410
2468
  }
2411
2469
 
2470
+ // --- Agents ---
2471
+ if (method === 'GET' && urlPath === '/api/agents') {
2472
+ try {
2473
+ const agentsDir = path.join(HOME, '.openclaw', 'agents');
2474
+ const agents = fs.readdirSync(agentsDir, { withFileTypes: true })
2475
+ .filter(e => e.isDirectory())
2476
+ .map(e => e.name);
2477
+ return send(res, 200, { agents });
2478
+ } catch {
2479
+ return send(res, 200, { agents: ['main'] });
2480
+ }
2481
+ }
2482
+
2412
2483
  // --- Workspaces ---
2413
2484
  if (method === 'GET' && urlPath === '/api/workspaces') {
2414
2485
  return handleGetWorkspaces(req, res);
@@ -3199,9 +3270,9 @@ function syncThreadUnreadCount(db, threadId) {
3199
3270
  // Helper: Parse session key
3200
3271
  function parseSessionKey(sessionKey) {
3201
3272
  if (!sessionKey) return null;
3202
- const match = sessionKey.match(/^agent:main:([^:]+):chat:([^:]+)$/);
3273
+ const match = sessionKey.match(/^agent:([^:]+):([^:]+):chat:([^:]+)$/);
3203
3274
  if (!match) return null; // Non-ClawChats keys — silently ignore
3204
- return { workspace: match[1], threadId: match[2] };
3275
+ return { agent: match[1], workspace: match[2], threadId: match[3] };
3205
3276
  }
3206
3277
 
3207
3278
  // Helper: Extract content from message
@@ -3541,7 +3612,9 @@ export function createApp(config = {}) {
3541
3612
  }
3542
3613
  const ws = _getWorkspaces();
3543
3614
  if (ws.workspaces[name]) return sendError(res, 409, 'Workspace already exists');
3544
- const workspace = { name, label: label || name, color: body.color || null, icon: body.icon || null, createdAt: Date.now() };
3615
+ let agent = 'main';
3616
+ try { agent = validateAgent(body.agent || 'main'); } catch { agent = 'main'; }
3617
+ const workspace = { name, label: label || name, color: body.color || null, icon: body.icon || null, agent, createdAt: Date.now() };
3545
3618
  ws.workspaces[name] = workspace;
3546
3619
  _setWorkspaces(ws);
3547
3620
  _getDb(name);
@@ -3556,8 +3629,38 @@ export function createApp(config = {}) {
3556
3629
  if (body.color !== undefined) ws.workspaces[params.name].color = body.color;
3557
3630
  if (body.icon !== undefined) ws.workspaces[params.name].icon = body.icon;
3558
3631
  if (body.lastThread !== undefined) ws.workspaces[params.name].lastThread = body.lastThread;
3632
+ let migratedThreads = 0;
3633
+ if (body.agent !== undefined) {
3634
+ let newAgent;
3635
+ try { newAgent = validateAgent(body.agent); } catch (e) { return sendError(res, 400, e.message); }
3636
+ const oldAgent = ws.workspaces[params.name].agent || 'main';
3637
+ if (newAgent !== oldAgent) {
3638
+ const db = _getDb(params.name);
3639
+ const threads = db.prepare(
3640
+ `SELECT id, session_key FROM threads WHERE session_key LIKE ?`
3641
+ ).all(`agent:${oldAgent}:${params.name}:chat:%`);
3642
+ db.prepare(`
3643
+ UPDATE threads
3644
+ SET session_key = replace(
3645
+ session_key,
3646
+ 'agent:' || ? || ':' || ? || ':chat:',
3647
+ 'agent:' || ? || ':' || ? || ':chat:'
3648
+ )
3649
+ WHERE session_key LIKE 'agent:' || ? || ':' || ? || ':chat:%'
3650
+ `).run(oldAgent, params.name, newAgent, params.name, oldAgent, params.name);
3651
+ for (const t of threads) cleanGatewaySession(t.session_key);
3652
+ ws.workspaces[params.name].agent = newAgent;
3653
+ migratedThreads = threads.length;
3654
+ _gatewayClient.broadcastToBrowsers(JSON.stringify({
3655
+ type: 'clawchats',
3656
+ event: 'workspace-agent-changed',
3657
+ workspace: params.name,
3658
+ agent: newAgent
3659
+ }));
3660
+ }
3661
+ }
3559
3662
  _setWorkspaces(ws);
3560
- send(res, 200, { workspace: ws.workspaces[params.name] });
3663
+ send(res, 200, { workspace: ws.workspaces[params.name], migratedThreads });
3561
3664
  }
3562
3665
 
3563
3666
  function _handleDeleteWorkspace(req, res, params) {
@@ -3570,7 +3673,8 @@ export function createApp(config = {}) {
3570
3673
  try { fs.unlinkSync(dbPath); } catch { /* ok */ }
3571
3674
  try { fs.unlinkSync(dbPath + '-wal'); } catch { /* ok */ }
3572
3675
  try { fs.unlinkSync(dbPath + '-shm'); } catch { /* ok */ }
3573
- const cleaned = cleanGatewaySessionsByPrefix(`agent:main:${params.name}:chat:`);
3676
+ const wsAgentForDelete = ws.workspaces[params.name]?.agent || 'main';
3677
+ const cleaned = cleanGatewaySessionsByPrefix(`agent:${wsAgentForDelete}:${params.name}:chat:`);
3574
3678
  if (cleaned > 0) console.log(`Cleaned ${cleaned} gateway sessions for workspace: ${params.name}`);
3575
3679
  delete ws.workspaces[params.name];
3576
3680
  _setWorkspaces(ws);
@@ -3664,7 +3768,8 @@ export function createApp(config = {}) {
3664
3768
  const ws = _getWorkspaces();
3665
3769
  const id = body.id || uuid();
3666
3770
  const now = Date.now();
3667
- const sessionKey = `agent:main:${ws.active}:chat:${id}`;
3771
+ const workspaceAgent = ws.workspaces[ws.active]?.agent || 'main';
3772
+ const sessionKey = `agent:${workspaceAgent}:${ws.active}:chat:${id}`;
3668
3773
  try {
3669
3774
  db.prepare('INSERT INTO threads (id, session_key, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?)').run(id, sessionKey, 'New chat', now, now);
3670
3775
  } catch (e) {
@@ -3709,9 +3814,11 @@ export function createApp(config = {}) {
3709
3814
  if (!thread) return sendError(res, 404, 'Thread not found');
3710
3815
  db.prepare('DELETE FROM threads WHERE id = ?').run(params.id);
3711
3816
  let sessionIdToDelete = thread.last_session_id;
3817
+ const tAgentMatch = (thread.session_key || '').match(/^agent:([^:]+):/);
3818
+ const tSessionsDir = getSessionsDirForAgent(tAgentMatch?.[1]);
3712
3819
  if (!sessionIdToDelete) {
3713
3820
  try {
3714
- const raw = fs.readFileSync(path.join(OPENCLAW_SESSIONS_DIR, 'sessions.json'), 'utf8');
3821
+ const raw = fs.readFileSync(path.join(tSessionsDir, 'sessions.json'), 'utf8');
3715
3822
  const store = JSON.parse(raw);
3716
3823
  const entry = store[thread.session_key];
3717
3824
  if (entry?.sessionId) sessionIdToDelete = entry.sessionId;
@@ -3719,7 +3826,7 @@ export function createApp(config = {}) {
3719
3826
  }
3720
3827
  cleanGatewaySession(thread.session_key);
3721
3828
  if (sessionIdToDelete) {
3722
- const jsonlPath = path.join(OPENCLAW_SESSIONS_DIR, `${sessionIdToDelete}.jsonl`);
3829
+ const jsonlPath = path.join(tSessionsDir, `${sessionIdToDelete}.jsonl`);
3723
3830
  try { fs.unlinkSync(jsonlPath); } catch { /* ok */ }
3724
3831
  }
3725
3832
  const uploadDir = path.join(_UPLOADS_DIR, params.id);
@@ -3810,7 +3917,7 @@ export function createApp(config = {}) {
3810
3917
  const db = _getActiveDb();
3811
3918
  const thread = db.prepare('SELECT * FROM threads WHERE id = ?').get(params.id);
3812
3919
  if (!thread) return sendError(res, 404, 'Thread not found');
3813
- const { preamble, method } = buildContextPreamble(db, params.id, thread.last_session_id);
3920
+ const { preamble, method } = buildContextPreamble(db, params.id, thread.last_session_id, thread.session_key);
3814
3921
  send(res, 200, { preamble, method });
3815
3922
  }
3816
3923
 
@@ -3862,6 +3969,7 @@ export function createApp(config = {}) {
3862
3969
  const importAll = db.transaction(() => {
3863
3970
  for (const t of body.threads) {
3864
3971
  if (!t.id) continue;
3972
+ // TODO: per-project agent — import uses agent:main for now
3865
3973
  const sessionKey = t.session_key || `agent:main:${ws.active}:chat:${t.id}`;
3866
3974
  const result = insertThread.run(t.id, sessionKey, t.title || 'Imported chat', t.pinned || 0, t.pin_order || 0, t.model || null, t.last_session_id || null, t.created_at || Date.now(), t.updated_at || Date.now());
3867
3975
  if (result.changes > 0) threadsImported++;
@@ -4612,6 +4720,14 @@ export function createApp(config = {}) {
4612
4720
  if (method === 'GET' && urlPath === '/api/settings') return _handleGetSettings(req, res);
4613
4721
  if (method === 'PUT' && urlPath === '/api/settings') return await _handleSaveSettings(req, res);
4614
4722
  if (method === 'POST' && urlPath === '/api/transcribe') return await handleTranscribe(req, res);
4723
+ if (method === 'GET' && urlPath === '/api/agents') {
4724
+ try {
4725
+ const agentsDir = path.join(HOME, '.openclaw', 'agents');
4726
+ const agents = fs.readdirSync(agentsDir, { withFileTypes: true })
4727
+ .filter(e => e.isDirectory()).map(e => e.name);
4728
+ return send(res, 200, { agents });
4729
+ } catch { return send(res, 200, { agents: ['main'] }); }
4730
+ }
4615
4731
  if (method === 'GET' && urlPath === '/api/workspaces') return _handleGetWorkspaces(req, res);
4616
4732
  if (method === 'POST' && urlPath === '/api/workspaces') return await _handleCreateWorkspace(req, res);
4617
4733
  if ((p = matchRoute(method, urlPath, 'PATCH /api/workspaces/:name'))) return await _handleUpdateWorkspace(req, res, p);