@clawchatsai/connector 0.0.69 → 0.0.71

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 +145 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.69",
3
+ "version": "0.0.71",
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) {
@@ -850,9 +903,6 @@ function handleDeleteWorkspace(req, res, params) {
850
903
  if (Object.keys(ws.workspaces).length <= 1) {
851
904
  return sendError(res, 400, 'Cannot delete the only workspace');
852
905
  }
853
- if (ws.active === params.name) {
854
- return sendError(res, 400, 'Cannot delete the active workspace');
855
- }
856
906
  // Close and remove DB
857
907
  closeDb(params.name);
858
908
  const dbPath = path.join(DATA_DIR, `${params.name}.db`);
@@ -861,12 +911,19 @@ function handleDeleteWorkspace(req, res, params) {
861
911
  try { fs.unlinkSync(dbPath + '-shm'); } catch { /* ok */ }
862
912
 
863
913
  // Clean all gateway sessions for this workspace
864
- const cleaned = cleanGatewaySessionsByPrefix(`agent:main:${params.name}:chat:`);
914
+ const wsAgentForDelete = ws.workspaces[params.name]?.agent || 'main';
915
+ const cleaned = cleanGatewaySessionsByPrefix(`agent:${wsAgentForDelete}:${params.name}:chat:`);
865
916
  if (cleaned > 0) {
866
917
  console.log(`Cleaned ${cleaned} gateway sessions for workspace: ${params.name}`);
867
918
  }
868
919
 
869
920
  delete ws.workspaces[params.name];
921
+
922
+ // If this was the active workspace, switch active to the first remaining one
923
+ if (ws.active === params.name) {
924
+ ws.active = Object.keys(ws.workspaces)[0] || null;
925
+ }
926
+
870
927
  setWorkspaces(ws);
871
928
  send(res, 200, { ok: true });
872
929
  }
@@ -1004,7 +1061,8 @@ async function handleCreateThread(req, res) {
1004
1061
  const ws = getWorkspaces();
1005
1062
  const id = body.id || uuid();
1006
1063
  const now = Date.now();
1007
- const sessionKey = `agent:main:${ws.active}:chat:${id}`;
1064
+ const workspaceAgent = ws.workspaces[ws.active]?.agent || 'main';
1065
+ const sessionKey = `agent:${workspaceAgent}:${ws.active}:chat:${id}`;
1008
1066
 
1009
1067
  try {
1010
1068
  db.prepare(
@@ -1065,9 +1123,11 @@ function handleDeleteThread(req, res, params) {
1065
1123
 
1066
1124
  // Look up sessionId from SQLite or sessions.json as fallback
1067
1125
  let sessionIdToDelete = thread.last_session_id;
1126
+ const threadAgentMatch = (thread.session_key || '').match(/^agent:([^:]+):/);
1127
+ const threadSessionsDir = getSessionsDirForAgent(threadAgentMatch?.[1]);
1068
1128
  if (!sessionIdToDelete) {
1069
1129
  try {
1070
- const raw = fs.readFileSync(path.join(OPENCLAW_SESSIONS_DIR, 'sessions.json'), 'utf8');
1130
+ const raw = fs.readFileSync(path.join(threadSessionsDir, 'sessions.json'), 'utf8');
1071
1131
  const store = JSON.parse(raw);
1072
1132
  const entry = store[thread.session_key];
1073
1133
  if (entry?.sessionId) sessionIdToDelete = entry.sessionId;
@@ -1079,7 +1139,7 @@ function handleDeleteThread(req, res, params) {
1079
1139
 
1080
1140
  // If cleanGatewaySession didn't find it but we have a sessionId, delete transcript directly
1081
1141
  if (sessionIdToDelete) {
1082
- const jsonlPath = path.join(OPENCLAW_SESSIONS_DIR, `${sessionIdToDelete}.jsonl`);
1142
+ const jsonlPath = path.join(threadSessionsDir, `${sessionIdToDelete}.jsonl`);
1083
1143
  try { fs.unlinkSync(jsonlPath); } catch { /* ok */ }
1084
1144
  }
1085
1145
 
@@ -1205,7 +1265,7 @@ function handleContextFill(req, res, params) {
1205
1265
  const thread = db.prepare('SELECT * FROM threads WHERE id = ?').get(params.id);
1206
1266
  if (!thread) return sendError(res, 404, 'Thread not found');
1207
1267
 
1208
- const { preamble, method } = buildContextPreamble(db, params.id, thread.last_session_id);
1268
+ const { preamble, method } = buildContextPreamble(db, params.id, thread.last_session_id, thread.session_key);
1209
1269
  send(res, 200, { preamble, method });
1210
1270
  }
1211
1271
 
@@ -1300,6 +1360,7 @@ async function handleImport(req, res) {
1300
1360
  const importAll = db.transaction(() => {
1301
1361
  for (const t of body.threads) {
1302
1362
  if (!t.id) continue;
1363
+ // TODO: per-project agent — import uses agent:main for now
1303
1364
  const sessionKey = t.session_key || `agent:main:${ws.active}:chat:${t.id}`;
1304
1365
  const result = insertThread.run(
1305
1366
  t.id, sessionKey, t.title || 'Imported chat',
@@ -2409,6 +2470,19 @@ async function _handleRequestImpl(req, res) {
2409
2470
  return await handleTranscribe(req, res);
2410
2471
  }
2411
2472
 
2473
+ // --- Agents ---
2474
+ if (method === 'GET' && urlPath === '/api/agents') {
2475
+ try {
2476
+ const agentsDir = path.join(HOME, '.openclaw', 'agents');
2477
+ const agents = fs.readdirSync(agentsDir, { withFileTypes: true })
2478
+ .filter(e => e.isDirectory())
2479
+ .map(e => e.name);
2480
+ return send(res, 200, { agents });
2481
+ } catch {
2482
+ return send(res, 200, { agents: ['main'] });
2483
+ }
2484
+ }
2485
+
2412
2486
  // --- Workspaces ---
2413
2487
  if (method === 'GET' && urlPath === '/api/workspaces') {
2414
2488
  return handleGetWorkspaces(req, res);
@@ -3199,9 +3273,9 @@ function syncThreadUnreadCount(db, threadId) {
3199
3273
  // Helper: Parse session key
3200
3274
  function parseSessionKey(sessionKey) {
3201
3275
  if (!sessionKey) return null;
3202
- const match = sessionKey.match(/^agent:main:([^:]+):chat:([^:]+)$/);
3276
+ const match = sessionKey.match(/^agent:([^:]+):([^:]+):chat:([^:]+)$/);
3203
3277
  if (!match) return null; // Non-ClawChats keys — silently ignore
3204
- return { workspace: match[1], threadId: match[2] };
3278
+ return { agent: match[1], workspace: match[2], threadId: match[3] };
3205
3279
  }
3206
3280
 
3207
3281
  // Helper: Extract content from message
@@ -3541,7 +3615,9 @@ export function createApp(config = {}) {
3541
3615
  }
3542
3616
  const ws = _getWorkspaces();
3543
3617
  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() };
3618
+ let agent = 'main';
3619
+ try { agent = validateAgent(body.agent || 'main'); } catch { agent = 'main'; }
3620
+ const workspace = { name, label: label || name, color: body.color || null, icon: body.icon || null, agent, createdAt: Date.now() };
3545
3621
  ws.workspaces[name] = workspace;
3546
3622
  _setWorkspaces(ws);
3547
3623
  _getDb(name);
@@ -3556,23 +3632,54 @@ export function createApp(config = {}) {
3556
3632
  if (body.color !== undefined) ws.workspaces[params.name].color = body.color;
3557
3633
  if (body.icon !== undefined) ws.workspaces[params.name].icon = body.icon;
3558
3634
  if (body.lastThread !== undefined) ws.workspaces[params.name].lastThread = body.lastThread;
3635
+ let migratedThreads = 0;
3636
+ if (body.agent !== undefined) {
3637
+ let newAgent;
3638
+ try { newAgent = validateAgent(body.agent); } catch (e) { return sendError(res, 400, e.message); }
3639
+ const oldAgent = ws.workspaces[params.name].agent || 'main';
3640
+ if (newAgent !== oldAgent) {
3641
+ const db = _getDb(params.name);
3642
+ const threads = db.prepare(
3643
+ `SELECT id, session_key FROM threads WHERE session_key LIKE ?`
3644
+ ).all(`agent:${oldAgent}:${params.name}:chat:%`);
3645
+ db.prepare(`
3646
+ UPDATE threads
3647
+ SET session_key = replace(
3648
+ session_key,
3649
+ 'agent:' || ? || ':' || ? || ':chat:',
3650
+ 'agent:' || ? || ':' || ? || ':chat:'
3651
+ )
3652
+ WHERE session_key LIKE 'agent:' || ? || ':' || ? || ':chat:%'
3653
+ `).run(oldAgent, params.name, newAgent, params.name, oldAgent, params.name);
3654
+ for (const t of threads) cleanGatewaySession(t.session_key);
3655
+ ws.workspaces[params.name].agent = newAgent;
3656
+ migratedThreads = threads.length;
3657
+ _gatewayClient.broadcastToBrowsers(JSON.stringify({
3658
+ type: 'clawchats',
3659
+ event: 'workspace-agent-changed',
3660
+ workspace: params.name,
3661
+ agent: newAgent
3662
+ }));
3663
+ }
3664
+ }
3559
3665
  _setWorkspaces(ws);
3560
- send(res, 200, { workspace: ws.workspaces[params.name] });
3666
+ send(res, 200, { workspace: ws.workspaces[params.name], migratedThreads });
3561
3667
  }
3562
3668
 
3563
3669
  function _handleDeleteWorkspace(req, res, params) {
3564
3670
  const ws = _getWorkspaces();
3565
3671
  if (!ws.workspaces[params.name]) return sendError(res, 404, 'Workspace not found');
3566
3672
  if (Object.keys(ws.workspaces).length <= 1) return sendError(res, 400, 'Cannot delete the only workspace');
3567
- if (ws.active === params.name) return sendError(res, 400, 'Cannot delete the active workspace');
3568
3673
  _closeDb(params.name);
3569
3674
  const dbPath = path.join(_DATA_DIR, `${params.name}.db`);
3570
3675
  try { fs.unlinkSync(dbPath); } catch { /* ok */ }
3571
3676
  try { fs.unlinkSync(dbPath + '-wal'); } catch { /* ok */ }
3572
3677
  try { fs.unlinkSync(dbPath + '-shm'); } catch { /* ok */ }
3573
- const cleaned = cleanGatewaySessionsByPrefix(`agent:main:${params.name}:chat:`);
3678
+ const wsAgentForDelete = ws.workspaces[params.name]?.agent || 'main';
3679
+ const cleaned = cleanGatewaySessionsByPrefix(`agent:${wsAgentForDelete}:${params.name}:chat:`);
3574
3680
  if (cleaned > 0) console.log(`Cleaned ${cleaned} gateway sessions for workspace: ${params.name}`);
3575
3681
  delete ws.workspaces[params.name];
3682
+ if (ws.active === params.name) ws.active = Object.keys(ws.workspaces)[0] || null;
3576
3683
  _setWorkspaces(ws);
3577
3684
  send(res, 200, { ok: true });
3578
3685
  }
@@ -3664,7 +3771,8 @@ export function createApp(config = {}) {
3664
3771
  const ws = _getWorkspaces();
3665
3772
  const id = body.id || uuid();
3666
3773
  const now = Date.now();
3667
- const sessionKey = `agent:main:${ws.active}:chat:${id}`;
3774
+ const workspaceAgent = ws.workspaces[ws.active]?.agent || 'main';
3775
+ const sessionKey = `agent:${workspaceAgent}:${ws.active}:chat:${id}`;
3668
3776
  try {
3669
3777
  db.prepare('INSERT INTO threads (id, session_key, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?)').run(id, sessionKey, 'New chat', now, now);
3670
3778
  } catch (e) {
@@ -3709,9 +3817,11 @@ export function createApp(config = {}) {
3709
3817
  if (!thread) return sendError(res, 404, 'Thread not found');
3710
3818
  db.prepare('DELETE FROM threads WHERE id = ?').run(params.id);
3711
3819
  let sessionIdToDelete = thread.last_session_id;
3820
+ const tAgentMatch = (thread.session_key || '').match(/^agent:([^:]+):/);
3821
+ const tSessionsDir = getSessionsDirForAgent(tAgentMatch?.[1]);
3712
3822
  if (!sessionIdToDelete) {
3713
3823
  try {
3714
- const raw = fs.readFileSync(path.join(OPENCLAW_SESSIONS_DIR, 'sessions.json'), 'utf8');
3824
+ const raw = fs.readFileSync(path.join(tSessionsDir, 'sessions.json'), 'utf8');
3715
3825
  const store = JSON.parse(raw);
3716
3826
  const entry = store[thread.session_key];
3717
3827
  if (entry?.sessionId) sessionIdToDelete = entry.sessionId;
@@ -3719,7 +3829,7 @@ export function createApp(config = {}) {
3719
3829
  }
3720
3830
  cleanGatewaySession(thread.session_key);
3721
3831
  if (sessionIdToDelete) {
3722
- const jsonlPath = path.join(OPENCLAW_SESSIONS_DIR, `${sessionIdToDelete}.jsonl`);
3832
+ const jsonlPath = path.join(tSessionsDir, `${sessionIdToDelete}.jsonl`);
3723
3833
  try { fs.unlinkSync(jsonlPath); } catch { /* ok */ }
3724
3834
  }
3725
3835
  const uploadDir = path.join(_UPLOADS_DIR, params.id);
@@ -3810,7 +3920,7 @@ export function createApp(config = {}) {
3810
3920
  const db = _getActiveDb();
3811
3921
  const thread = db.prepare('SELECT * FROM threads WHERE id = ?').get(params.id);
3812
3922
  if (!thread) return sendError(res, 404, 'Thread not found');
3813
- const { preamble, method } = buildContextPreamble(db, params.id, thread.last_session_id);
3923
+ const { preamble, method } = buildContextPreamble(db, params.id, thread.last_session_id, thread.session_key);
3814
3924
  send(res, 200, { preamble, method });
3815
3925
  }
3816
3926
 
@@ -3862,6 +3972,7 @@ export function createApp(config = {}) {
3862
3972
  const importAll = db.transaction(() => {
3863
3973
  for (const t of body.threads) {
3864
3974
  if (!t.id) continue;
3975
+ // TODO: per-project agent — import uses agent:main for now
3865
3976
  const sessionKey = t.session_key || `agent:main:${ws.active}:chat:${t.id}`;
3866
3977
  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
3978
  if (result.changes > 0) threadsImported++;
@@ -4612,6 +4723,14 @@ export function createApp(config = {}) {
4612
4723
  if (method === 'GET' && urlPath === '/api/settings') return _handleGetSettings(req, res);
4613
4724
  if (method === 'PUT' && urlPath === '/api/settings') return await _handleSaveSettings(req, res);
4614
4725
  if (method === 'POST' && urlPath === '/api/transcribe') return await handleTranscribe(req, res);
4726
+ if (method === 'GET' && urlPath === '/api/agents') {
4727
+ try {
4728
+ const agentsDir = path.join(HOME, '.openclaw', 'agents');
4729
+ const agents = fs.readdirSync(agentsDir, { withFileTypes: true })
4730
+ .filter(e => e.isDirectory()).map(e => e.name);
4731
+ return send(res, 200, { agents });
4732
+ } catch { return send(res, 200, { agents: ['main'] }); }
4733
+ }
4615
4734
  if (method === 'GET' && urlPath === '/api/workspaces') return _handleGetWorkspaces(req, res);
4616
4735
  if (method === 'POST' && urlPath === '/api/workspaces') return await _handleCreateWorkspace(req, res);
4617
4736
  if ((p = matchRoute(method, urlPath, 'PATCH /api/workspaces/:name'))) return await _handleUpdateWorkspace(req, res, p);