commanderclaw 1.1.15 → 1.1.20
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/binaries/darwin-arm64/coordination-server +0 -0
- package/dist/plugin/client.d.ts.map +1 -1
- package/dist/plugin/index.cjs +374 -1
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.mjs +374 -1
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/plugin/session/backend-detector.d.ts +8 -0
- package/dist/plugin/session/backend-detector.d.ts.map +1 -0
- package/dist/plugin/session/manager.d.ts +49 -0
- package/dist/plugin/session/manager.d.ts.map +1 -0
- package/dist/plugin/session/openclaw-strategy.d.ts +25 -0
- package/dist/plugin/session/openclaw-strategy.d.ts.map +1 -0
- package/dist/plugin/session/simple-strategy.d.ts +19 -0
- package/dist/plugin/session/simple-strategy.d.ts.map +1 -0
- package/dist/plugin/session/strategy.d.ts +39 -0
- package/dist/plugin/session/strategy.d.ts.map +1 -0
- package/dist/plugin/session/types.d.ts +39 -0
- package/dist/plugin/session/types.d.ts.map +1 -0
- package/dist/plugin/types.d.ts +11 -1
- package/dist/plugin/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/web/assets/index-CDWMe0mH.js +9 -0
- package/web/assets/index-jgz3Usc3.js +9 -0
- package/web/index.html +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwaH,QAAA,MAAM,MAAM;;;;;kBAKI,GAAG;CAQlB,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/plugin/index.mjs
CHANGED
|
@@ -138,6 +138,12 @@ class CommanderClawClient {
|
|
|
138
138
|
this.emit("chat", { type: "chat", data: msg });
|
|
139
139
|
break;
|
|
140
140
|
}
|
|
141
|
+
case "SESSION_SWITCH": {
|
|
142
|
+
const payload = msg.payload;
|
|
143
|
+
console.log(`[CommanderClawClient] SESSION_SWITCH: sessionId=${payload.sessionId}, action=${payload.action || "legacy"}`);
|
|
144
|
+
this.emit("session_switch", { type: "session_switch", data: payload });
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
141
147
|
default: {
|
|
142
148
|
console.log(`[CommanderClawClient] Unknown message type: ${msg.type}`);
|
|
143
149
|
const handler = this.messageHandlers.get(msg.type);
|
|
@@ -4270,6 +4276,314 @@ Note: Some commands may require King-level permissions.`,
|
|
|
4270
4276
|
};
|
|
4271
4277
|
}
|
|
4272
4278
|
|
|
4279
|
+
/**
|
|
4280
|
+
* OpenClaw Session Strategy
|
|
4281
|
+
*
|
|
4282
|
+
* Uses ACP Runtime to create and manage sessions for OpenClaw-based agents (QClaw, OpenCode, etc.).
|
|
4283
|
+
*/
|
|
4284
|
+
class OpenClawSessionStrategy {
|
|
4285
|
+
name;
|
|
4286
|
+
runtime = null;
|
|
4287
|
+
sessions = new Map();
|
|
4288
|
+
activeSessionId = null;
|
|
4289
|
+
initialized = false;
|
|
4290
|
+
initPromise = null;
|
|
4291
|
+
constructor(backendType) {
|
|
4292
|
+
this.name = backendType;
|
|
4293
|
+
this.initPromise = this.initRuntime();
|
|
4294
|
+
}
|
|
4295
|
+
async initRuntime() {
|
|
4296
|
+
try {
|
|
4297
|
+
// Dynamic import to handle cases where openclaw is not available
|
|
4298
|
+
const openclaw = await import('openclaw/plugin-sdk');
|
|
4299
|
+
const backend = openclaw.requireAcpRuntimeBackend();
|
|
4300
|
+
this.runtime = backend?.runtime;
|
|
4301
|
+
this.initialized = true;
|
|
4302
|
+
console.log(`[CommanderClaw] ACP Runtime initialized for ${this.name}`);
|
|
4303
|
+
}
|
|
4304
|
+
catch (e) {
|
|
4305
|
+
console.warn(`[CommanderClaw] ACP Runtime not available for ${this.name}:`, e);
|
|
4306
|
+
this.initialized = true; // Mark as initialized even on failure
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
async ensureInitialized() {
|
|
4310
|
+
if (!this.initialized && this.initPromise) {
|
|
4311
|
+
await this.initPromise;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
isAvailable() {
|
|
4315
|
+
return !!this.runtime;
|
|
4316
|
+
}
|
|
4317
|
+
async createSession(sessionId, options) {
|
|
4318
|
+
await this.ensureInitialized();
|
|
4319
|
+
if (!this.runtime) {
|
|
4320
|
+
throw new Error('ACP Runtime not available');
|
|
4321
|
+
}
|
|
4322
|
+
const sessionKey = options?.sessionKey || `commanderclaw:${sessionId}`;
|
|
4323
|
+
try {
|
|
4324
|
+
const handle = await this.runtime.ensureSession({
|
|
4325
|
+
sessionKey,
|
|
4326
|
+
agent: 'default',
|
|
4327
|
+
mode: options?.mode || 'persistent',
|
|
4328
|
+
cwd: options?.cwd,
|
|
4329
|
+
env: options?.env,
|
|
4330
|
+
});
|
|
4331
|
+
const sessionHandle = {
|
|
4332
|
+
sessionId,
|
|
4333
|
+
localSessionKey: handle.sessionKey,
|
|
4334
|
+
createdAt: new Date(),
|
|
4335
|
+
metadata: {
|
|
4336
|
+
backendSessionId: handle.backendSessionId,
|
|
4337
|
+
agentSessionId: handle.agentSessionId,
|
|
4338
|
+
cwd: handle.cwd,
|
|
4339
|
+
},
|
|
4340
|
+
};
|
|
4341
|
+
this.sessions.set(sessionId, sessionHandle);
|
|
4342
|
+
this.activeSessionId = sessionId;
|
|
4343
|
+
console.log(`[CommanderClaw] Session created via ACP Runtime: ${sessionId} -> ${handle.sessionKey}`);
|
|
4344
|
+
return sessionHandle;
|
|
4345
|
+
}
|
|
4346
|
+
catch (e) {
|
|
4347
|
+
console.error(`[CommanderClaw] Failed to create session ${sessionId}:`, e);
|
|
4348
|
+
throw e;
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
getSession(sessionId) {
|
|
4352
|
+
return this.sessions.get(sessionId) || null;
|
|
4353
|
+
}
|
|
4354
|
+
switchSession(sessionId) {
|
|
4355
|
+
if (!this.sessions.has(sessionId)) {
|
|
4356
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
4357
|
+
}
|
|
4358
|
+
this.activeSessionId = sessionId;
|
|
4359
|
+
console.log(`[CommanderClaw] Switched to session: ${sessionId}`);
|
|
4360
|
+
}
|
|
4361
|
+
getActiveSession() {
|
|
4362
|
+
if (!this.activeSessionId)
|
|
4363
|
+
return null;
|
|
4364
|
+
return this.sessions.get(this.activeSessionId) || null;
|
|
4365
|
+
}
|
|
4366
|
+
async closeSession(sessionId) {
|
|
4367
|
+
const session = this.sessions.get(sessionId);
|
|
4368
|
+
if (!session)
|
|
4369
|
+
return;
|
|
4370
|
+
if (this.runtime?.close) {
|
|
4371
|
+
try {
|
|
4372
|
+
await this.runtime.close({
|
|
4373
|
+
handle: { sessionKey: session.localSessionKey },
|
|
4374
|
+
reason: 'Session closed by CommanderClaw',
|
|
4375
|
+
});
|
|
4376
|
+
}
|
|
4377
|
+
catch (e) {
|
|
4378
|
+
console.warn(`[CommanderClaw] Failed to close session ${sessionId} via ACP Runtime:`, e);
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
this.sessions.delete(sessionId);
|
|
4382
|
+
if (this.activeSessionId === sessionId) {
|
|
4383
|
+
this.activeSessionId = null;
|
|
4384
|
+
}
|
|
4385
|
+
console.log(`[CommanderClaw] Session closed: ${sessionId}`);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
/**
|
|
4390
|
+
* Simple Session Strategy
|
|
4391
|
+
*
|
|
4392
|
+
* Fallback strategy that uses in-memory session mapping.
|
|
4393
|
+
* Used when ACP Runtime is not available.
|
|
4394
|
+
*/
|
|
4395
|
+
class SimpleSessionStrategy {
|
|
4396
|
+
name = 'simple';
|
|
4397
|
+
sessions = new Map();
|
|
4398
|
+
activeSessionId = null;
|
|
4399
|
+
isAvailable() {
|
|
4400
|
+
return true; // Always available as fallback
|
|
4401
|
+
}
|
|
4402
|
+
async createSession(sessionId, options) {
|
|
4403
|
+
const existing = this.sessions.get(sessionId);
|
|
4404
|
+
if (existing) {
|
|
4405
|
+
this.activeSessionId = sessionId;
|
|
4406
|
+
return existing;
|
|
4407
|
+
}
|
|
4408
|
+
const sessionKey = options?.sessionKey || `simple:${sessionId}:${Date.now()}`;
|
|
4409
|
+
const sessionHandle = {
|
|
4410
|
+
sessionId,
|
|
4411
|
+
localSessionKey: sessionKey,
|
|
4412
|
+
createdAt: new Date(),
|
|
4413
|
+
};
|
|
4414
|
+
this.sessions.set(sessionId, sessionHandle);
|
|
4415
|
+
this.activeSessionId = sessionId;
|
|
4416
|
+
console.log(`[CommanderClaw] Simple session created: ${sessionId} -> ${sessionKey}`);
|
|
4417
|
+
return sessionHandle;
|
|
4418
|
+
}
|
|
4419
|
+
getSession(sessionId) {
|
|
4420
|
+
return this.sessions.get(sessionId) || null;
|
|
4421
|
+
}
|
|
4422
|
+
switchSession(sessionId) {
|
|
4423
|
+
if (!this.sessions.has(sessionId)) {
|
|
4424
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
4425
|
+
}
|
|
4426
|
+
this.activeSessionId = sessionId;
|
|
4427
|
+
console.log(`[CommanderClaw] Switched to simple session: ${sessionId}`);
|
|
4428
|
+
}
|
|
4429
|
+
getActiveSession() {
|
|
4430
|
+
if (!this.activeSessionId)
|
|
4431
|
+
return null;
|
|
4432
|
+
return this.sessions.get(this.activeSessionId) || null;
|
|
4433
|
+
}
|
|
4434
|
+
async closeSession(sessionId) {
|
|
4435
|
+
this.sessions.delete(sessionId);
|
|
4436
|
+
if (this.activeSessionId === sessionId) {
|
|
4437
|
+
this.activeSessionId = null;
|
|
4438
|
+
}
|
|
4439
|
+
console.log(`[CommanderClaw] Simple session closed: ${sessionId}`);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
/**
|
|
4444
|
+
* Backend Detector
|
|
4445
|
+
*
|
|
4446
|
+
* Detects the ACP backend type for the current environment.
|
|
4447
|
+
*/
|
|
4448
|
+
function detectBackend() {
|
|
4449
|
+
// 1. Check environment variables
|
|
4450
|
+
if (process.env.QCLAW_BACKEND)
|
|
4451
|
+
return 'qclaw';
|
|
4452
|
+
if (process.env.OPENCODE_BACKEND)
|
|
4453
|
+
return 'opencode';
|
|
4454
|
+
// 2. Try to load OpenClaw SDK and detect backend
|
|
4455
|
+
try {
|
|
4456
|
+
// Use require for synchronous detection
|
|
4457
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
4458
|
+
const openclaw = require('openclaw/plugin-sdk');
|
|
4459
|
+
const backend = openclaw.requireAcpRuntimeBackend();
|
|
4460
|
+
if (!backend)
|
|
4461
|
+
return 'simple';
|
|
4462
|
+
const id = (backend.id || '').toLowerCase();
|
|
4463
|
+
if (id.includes('qclaw'))
|
|
4464
|
+
return 'qclaw';
|
|
4465
|
+
if (id.includes('opencode'))
|
|
4466
|
+
return 'opencode';
|
|
4467
|
+
if (id.includes('claude'))
|
|
4468
|
+
return 'claude';
|
|
4469
|
+
if (id.includes('cursor'))
|
|
4470
|
+
return 'cursor';
|
|
4471
|
+
// Has backend but unknown type
|
|
4472
|
+
return 'custom';
|
|
4473
|
+
}
|
|
4474
|
+
catch {
|
|
4475
|
+
// OpenClaw SDK not available or backend not registered
|
|
4476
|
+
}
|
|
4477
|
+
return 'simple';
|
|
4478
|
+
}
|
|
4479
|
+
|
|
4480
|
+
/**
|
|
4481
|
+
* Session Manager
|
|
4482
|
+
*
|
|
4483
|
+
* Manages session strategies and provides a unified interface for session operations.
|
|
4484
|
+
*/
|
|
4485
|
+
class SessionManager {
|
|
4486
|
+
activeStrategy = null;
|
|
4487
|
+
backendType = 'unknown';
|
|
4488
|
+
constructor() {
|
|
4489
|
+
this.initStrategy();
|
|
4490
|
+
}
|
|
4491
|
+
initStrategy() {
|
|
4492
|
+
const backend = detectBackend();
|
|
4493
|
+
this.backendType = backend;
|
|
4494
|
+
console.log(`[CommanderClaw] Detected backend: ${backend}`);
|
|
4495
|
+
if (backend === 'simple') {
|
|
4496
|
+
// No ACP Runtime available, use simple strategy
|
|
4497
|
+
this.activeStrategy = new SimpleSessionStrategy();
|
|
4498
|
+
console.log(`[CommanderClaw] Using strategy: simple (no ACP Runtime)`);
|
|
4499
|
+
}
|
|
4500
|
+
else {
|
|
4501
|
+
// Try OpenClaw strategy first
|
|
4502
|
+
const openclawStrategy = new OpenClawSessionStrategy(backend);
|
|
4503
|
+
// Check availability asynchronously
|
|
4504
|
+
openclawStrategy['initPromise']?.then(() => {
|
|
4505
|
+
if (openclawStrategy.isAvailable()) {
|
|
4506
|
+
console.log(`[CommanderClaw] Using strategy: ${backend} (ACP Runtime)`);
|
|
4507
|
+
}
|
|
4508
|
+
else {
|
|
4509
|
+
console.warn(`[CommanderClaw] OpenClaw strategy not available, using simple fallback`);
|
|
4510
|
+
this.activeStrategy = new SimpleSessionStrategy();
|
|
4511
|
+
}
|
|
4512
|
+
});
|
|
4513
|
+
this.activeStrategy = openclawStrategy;
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
/**
|
|
4517
|
+
* Get the backend type that was detected
|
|
4518
|
+
*/
|
|
4519
|
+
getBackendType() {
|
|
4520
|
+
return this.backendType;
|
|
4521
|
+
}
|
|
4522
|
+
/**
|
|
4523
|
+
* Get the active strategy name
|
|
4524
|
+
*/
|
|
4525
|
+
getStrategyName() {
|
|
4526
|
+
return this.activeStrategy?.name || 'none';
|
|
4527
|
+
}
|
|
4528
|
+
/**
|
|
4529
|
+
* Get the active strategy
|
|
4530
|
+
*/
|
|
4531
|
+
getActiveStrategy() {
|
|
4532
|
+
return this.activeStrategy;
|
|
4533
|
+
}
|
|
4534
|
+
/**
|
|
4535
|
+
* Create a new session
|
|
4536
|
+
*/
|
|
4537
|
+
async createSession(sessionId, options) {
|
|
4538
|
+
if (!this.activeStrategy) {
|
|
4539
|
+
console.warn('[CommanderClaw] No session strategy available');
|
|
4540
|
+
return null;
|
|
4541
|
+
}
|
|
4542
|
+
try {
|
|
4543
|
+
return await this.activeStrategy.createSession(sessionId, options);
|
|
4544
|
+
}
|
|
4545
|
+
catch (e) {
|
|
4546
|
+
console.error(`[CommanderClaw] Failed to create session ${sessionId}:`, e);
|
|
4547
|
+
return null;
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
/**
|
|
4551
|
+
* Get an existing session
|
|
4552
|
+
*/
|
|
4553
|
+
getSession(sessionId) {
|
|
4554
|
+
return this.activeStrategy?.getSession(sessionId) || null;
|
|
4555
|
+
}
|
|
4556
|
+
/**
|
|
4557
|
+
* Switch to an existing session
|
|
4558
|
+
*/
|
|
4559
|
+
switchSession(sessionId) {
|
|
4560
|
+
this.activeStrategy?.switchSession(sessionId);
|
|
4561
|
+
}
|
|
4562
|
+
/**
|
|
4563
|
+
* Get the currently active session
|
|
4564
|
+
*/
|
|
4565
|
+
getActiveSession() {
|
|
4566
|
+
return this.activeStrategy?.getActiveSession() || null;
|
|
4567
|
+
}
|
|
4568
|
+
/**
|
|
4569
|
+
* Close a session
|
|
4570
|
+
*/
|
|
4571
|
+
async closeSession(sessionId) {
|
|
4572
|
+
await this.activeStrategy?.closeSession(sessionId);
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
// Singleton instance
|
|
4576
|
+
let sessionManager$1 = null;
|
|
4577
|
+
/**
|
|
4578
|
+
* Get the singleton SessionManager instance
|
|
4579
|
+
*/
|
|
4580
|
+
function getSessionManager() {
|
|
4581
|
+
if (!sessionManager$1) {
|
|
4582
|
+
sessionManager$1 = new SessionManager();
|
|
4583
|
+
}
|
|
4584
|
+
return sessionManager$1;
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4273
4587
|
/**
|
|
4274
4588
|
* CommanderClaw Channel Plugin for OpenClaw
|
|
4275
4589
|
*
|
|
@@ -4281,6 +4595,8 @@ const { setRuntime, getRuntime } = createPluginRuntimeStore("CommanderClaw runti
|
|
|
4281
4595
|
let client = null;
|
|
4282
4596
|
let pluginConfig = null;
|
|
4283
4597
|
let currentLog = null;
|
|
4598
|
+
let currentSessionKey = null;
|
|
4599
|
+
const sessionManager = getSessionManager();
|
|
4284
4600
|
const DEFAULT_CONFIG = {
|
|
4285
4601
|
deviceName: "OpenClaw Agent",
|
|
4286
4602
|
autoConnect: true,
|
|
@@ -4338,6 +4654,8 @@ async function handleChatMessage(msg) {
|
|
|
4338
4654
|
id: senderId,
|
|
4339
4655
|
},
|
|
4340
4656
|
});
|
|
4657
|
+
const sessionHandle = sessionManager.getActiveSession();
|
|
4658
|
+
const sessionKey = sessionHandle?.localSessionKey || currentSessionKey || route.sessionKey;
|
|
4341
4659
|
const ctxPayload = rt.channel.reply.finalizeInboundContext({
|
|
4342
4660
|
Body: content,
|
|
4343
4661
|
RawBody: content,
|
|
@@ -4346,7 +4664,7 @@ async function handleChatMessage(msg) {
|
|
|
4346
4664
|
From: `${CHANNEL_ID}:${senderId}`,
|
|
4347
4665
|
To: `${CHANNEL_ID}:${nodeId}`,
|
|
4348
4666
|
SenderId: senderId,
|
|
4349
|
-
SessionKey:
|
|
4667
|
+
SessionKey: sessionKey,
|
|
4350
4668
|
AccountId: "default",
|
|
4351
4669
|
ChatType: "direct",
|
|
4352
4670
|
ConversationLabel: `user:${senderId}`,
|
|
@@ -4421,6 +4739,61 @@ function initializeClient(config, log) {
|
|
|
4421
4739
|
client.on("chat", (event) => {
|
|
4422
4740
|
handleChatMessage(event.data);
|
|
4423
4741
|
});
|
|
4742
|
+
client.on("session_switch", async (event) => {
|
|
4743
|
+
const payload = event.data;
|
|
4744
|
+
log.info("SESSION_SWITCH received", {
|
|
4745
|
+
sessionId: payload.sessionId,
|
|
4746
|
+
sessionKey: payload.sessionKey,
|
|
4747
|
+
action: payload.action || "legacy",
|
|
4748
|
+
});
|
|
4749
|
+
const action = payload.action || "switch";
|
|
4750
|
+
switch (action) {
|
|
4751
|
+
case "create": {
|
|
4752
|
+
try {
|
|
4753
|
+
const handle = await sessionManager.createSession(payload.sessionId, { ...payload.options, sessionKey: payload.sessionKey });
|
|
4754
|
+
currentSessionKey = handle.localSessionKey;
|
|
4755
|
+
log.info("Session created", {
|
|
4756
|
+
sessionId: payload.sessionId,
|
|
4757
|
+
localSessionKey: handle.localSessionKey,
|
|
4758
|
+
});
|
|
4759
|
+
}
|
|
4760
|
+
catch (e) {
|
|
4761
|
+
log.error("Failed to create session", {
|
|
4762
|
+
sessionId: payload.sessionId,
|
|
4763
|
+
error: String(e),
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
4766
|
+
break;
|
|
4767
|
+
}
|
|
4768
|
+
case "switch": {
|
|
4769
|
+
const existing = sessionManager.getSession(payload.sessionId);
|
|
4770
|
+
if (existing) {
|
|
4771
|
+
sessionManager.switchSession(payload.sessionId);
|
|
4772
|
+
currentSessionKey = existing.localSessionKey;
|
|
4773
|
+
log.info("Session switched", { sessionId: payload.sessionId });
|
|
4774
|
+
}
|
|
4775
|
+
else {
|
|
4776
|
+
log.info("Session not found, creating", { sessionId: payload.sessionId });
|
|
4777
|
+
try {
|
|
4778
|
+
const handle = await sessionManager.createSession(payload.sessionId, { sessionKey: payload.sessionKey });
|
|
4779
|
+
currentSessionKey = handle.localSessionKey;
|
|
4780
|
+
}
|
|
4781
|
+
catch (e) {
|
|
4782
|
+
log.error("Failed to create session on switch", {
|
|
4783
|
+
sessionId: payload.sessionId,
|
|
4784
|
+
error: String(e),
|
|
4785
|
+
});
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
break;
|
|
4789
|
+
}
|
|
4790
|
+
case "close":
|
|
4791
|
+
await sessionManager.closeSession(payload.sessionId);
|
|
4792
|
+
currentSessionKey = null;
|
|
4793
|
+
log.info("Session closed", { sessionId: payload.sessionId });
|
|
4794
|
+
break;
|
|
4795
|
+
}
|
|
4796
|
+
});
|
|
4424
4797
|
client.connect().catch((error) => {
|
|
4425
4798
|
log.error("Failed to connect to CommanderClaw server", {
|
|
4426
4799
|
error: error.message,
|