@mclawnet/agent 0.2.0 → 0.2.1

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.
@@ -0,0 +1,780 @@
1
+ // src/config.ts
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir, hostname } from "os";
5
+ var CONFIG_DIR = join(homedir(), ".clawnet");
6
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+ var DEFAULT_HUB_URL = process.env.CLAWNET_DEFAULT_HUB_URL || "ws://localhost:3000/ws/agent";
8
+ var DEFAULTS = {
9
+ hubUrl: DEFAULT_HUB_URL,
10
+ token: "",
11
+ name: hostname(),
12
+ backendType: "claude-code"
13
+ };
14
+ function loadConfig(cliOpts = {}) {
15
+ let fileConfig = {};
16
+ if (existsSync(CONFIG_FILE)) {
17
+ try {
18
+ fileConfig = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
19
+ } catch {
20
+ }
21
+ }
22
+ return {
23
+ hubUrl: cliOpts.hubUrl ?? process.env.CLAWNET_HUB_URL ?? fileConfig.hubUrl ?? DEFAULTS.hubUrl,
24
+ token: cliOpts.token ?? process.env.CLAWNET_TOKEN ?? fileConfig.token ?? DEFAULTS.token,
25
+ name: cliOpts.name ?? process.env.CLAWNET_NAME ?? fileConfig.name ?? DEFAULTS.name,
26
+ backendType: cliOpts.backendType ?? process.env.CLAWNET_BACKEND_TYPE ?? fileConfig.backendType ?? DEFAULTS.backendType
27
+ };
28
+ }
29
+ function saveConfig(config) {
30
+ mkdirSync(CONFIG_DIR, { recursive: true });
31
+ let existing = {};
32
+ if (existsSync(CONFIG_FILE)) {
33
+ try {
34
+ existing = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
35
+ } catch {
36
+ }
37
+ }
38
+ const merged = { ...existing, ...config };
39
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
40
+ }
41
+
42
+ // src/hub-connection.ts
43
+ import { hostname as osHostname } from "os";
44
+ import WebSocket from "ws";
45
+ import {
46
+ HEARTBEAT_INTERVAL_MS,
47
+ DEFAULT_RECONNECT_MS,
48
+ MAX_RECONNECT_MS,
49
+ WS_CLOSE_INVALID_TOKEN
50
+ } from "@mclawnet/shared";
51
+ import { listRecoverableSwarms, recoverSwarm, listRoles, loadRole, listRecoverableSwarmIds, deleteSwarmSnapshot } from "@mclawnet/swarm";
52
+
53
+ // src/fs-handler.ts
54
+ import { readdir } from "fs/promises";
55
+ import { existsSync as existsSync2, readdirSync, statSync, readFileSync as readFileSync2 } from "fs";
56
+ import { homedir as homedir2 } from "os";
57
+ import { join as join2 } from "path";
58
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "__pycache__", ".next", ".nuxt", ".cache"]);
59
+ async function handleListDir(path) {
60
+ const target = path || "/";
61
+ const dirents = await readdir(target, { withFileTypes: true });
62
+ const entries = dirents.filter((d) => !(d.isDirectory() && SKIP_DIRS.has(d.name))).map((d) => ({
63
+ name: d.name,
64
+ type: d.isDirectory() ? "directory" : "file"
65
+ })).sort((a, b) => {
66
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
67
+ return a.name.localeCompare(b.name);
68
+ });
69
+ return { path: target, entries };
70
+ }
71
+ function getClaudeProjectsDir() {
72
+ return join2(homedir2(), ".claude", "projects");
73
+ }
74
+ function pathToProjectFolder(workDir) {
75
+ return workDir.replace(/:/g, "-").replace(/[/\\]/g, "-");
76
+ }
77
+ function extractWorkDirFromSessionFile(filePath) {
78
+ try {
79
+ const content = readFileSync2(filePath, "utf-8");
80
+ const lines = content.split("\n").filter((l) => l.trim());
81
+ for (const line of lines.slice(0, 5)) {
82
+ try {
83
+ const data = JSON.parse(line);
84
+ if (data.cwd) return data.cwd.replace(/\\/g, "/");
85
+ } catch {
86
+ }
87
+ }
88
+ } catch {
89
+ }
90
+ return null;
91
+ }
92
+ function getWorkDirFromProjectFolder(folderPath, folderName) {
93
+ try {
94
+ const files = readdirSync(folderPath);
95
+ for (const file of files) {
96
+ if (file.endsWith(".jsonl")) {
97
+ const workDir = extractWorkDirFromSessionFile(join2(folderPath, file));
98
+ if (workDir) return workDir;
99
+ }
100
+ }
101
+ } catch {
102
+ }
103
+ if (/^[A-Za-z]--/.test(folderName)) {
104
+ return folderName.replace(/^([A-Za-z])--/, "$1:/").replace(/-/g, "/");
105
+ }
106
+ if (folderName.startsWith("-")) {
107
+ return "/" + folderName.substring(1).replace(/-/g, "/");
108
+ }
109
+ return folderName.replace(/-/g, "/");
110
+ }
111
+ async function handleListFolders() {
112
+ const projectsDir = getClaudeProjectsDir();
113
+ const folders = [];
114
+ if (!existsSync2(projectsDir)) return { folders };
115
+ const entries = readdirSync(projectsDir);
116
+ for (const entry of entries) {
117
+ const entryPath = join2(projectsDir, entry);
118
+ let entryStat;
119
+ try {
120
+ entryStat = statSync(entryPath);
121
+ } catch {
122
+ continue;
123
+ }
124
+ if (!entryStat.isDirectory()) continue;
125
+ if (entry.includes("--crew-roles-")) continue;
126
+ const originalPath = getWorkDirFromProjectFolder(entryPath, entry);
127
+ let sessionCount = 0;
128
+ let lastModified = entryStat.mtime.getTime();
129
+ try {
130
+ const files = readdirSync(entryPath);
131
+ for (const file of files) {
132
+ if (file.endsWith(".jsonl")) {
133
+ sessionCount++;
134
+ try {
135
+ const fileStats = statSync(join2(entryPath, file));
136
+ if (fileStats.mtime.getTime() > lastModified) {
137
+ lastModified = fileStats.mtime.getTime();
138
+ }
139
+ } catch {
140
+ }
141
+ }
142
+ }
143
+ } catch {
144
+ }
145
+ folders.push({ path: originalPath, sessionCount, lastModified });
146
+ }
147
+ folders.sort((a, b) => (b.lastModified ?? 0) - (a.lastModified ?? 0));
148
+ return { folders };
149
+ }
150
+ async function handleListHistorySessions(workDir) {
151
+ const projectsDir = getClaudeProjectsDir();
152
+ const projectFolder = pathToProjectFolder(workDir);
153
+ const projectPath = join2(projectsDir, projectFolder);
154
+ if (!existsSync2(projectPath)) {
155
+ return { workDir, sessions: [] };
156
+ }
157
+ const sessions = [];
158
+ const files = readdirSync(projectPath);
159
+ for (const file of files) {
160
+ if (!file.endsWith(".jsonl")) continue;
161
+ const sessionId = file.replace(".jsonl", "");
162
+ const filePath = join2(projectPath, file);
163
+ let fileStats;
164
+ try {
165
+ fileStats = statSync(filePath);
166
+ } catch {
167
+ continue;
168
+ }
169
+ let title = "";
170
+ let hasUserMessage = false;
171
+ let customTitle = "";
172
+ let jsonlSummary = "";
173
+ try {
174
+ const content = readFileSync2(filePath, "utf-8");
175
+ const lines = content.split("\n").filter((l) => l.trim());
176
+ for (const line of lines) {
177
+ try {
178
+ const data = JSON.parse(line);
179
+ if (!hasUserMessage && data.type === "user" && data.message?.content) {
180
+ const text = typeof data.message.content === "string" ? data.message.content : data.message.content[0]?.text || "";
181
+ if (text.trim()) {
182
+ title = text.substring(0, 100);
183
+ hasUserMessage = true;
184
+ }
185
+ }
186
+ if (data.type === "custom-title" && data.customTitle) {
187
+ customTitle = data.customTitle;
188
+ }
189
+ if (data.type === "summary" && data.summary) {
190
+ jsonlSummary = data.summary;
191
+ }
192
+ } catch {
193
+ }
194
+ }
195
+ } catch {
196
+ }
197
+ if (hasUserMessage) {
198
+ sessions.push({
199
+ sessionId,
200
+ title: customTitle || jsonlSummary || title || sessionId.slice(0, 8),
201
+ workDir,
202
+ lastModified: fileStats.mtime.getTime()
203
+ });
204
+ }
205
+ }
206
+ sessions.sort((a, b) => (b.lastModified ?? 0) - (a.lastModified ?? 0));
207
+ return { workDir, sessions };
208
+ }
209
+ async function handleLoadSessionHistory(workDir, claudeSessionId) {
210
+ const projectsDir = getClaudeProjectsDir();
211
+ const projectFolder = pathToProjectFolder(workDir);
212
+ const filePath = join2(projectsDir, projectFolder, `${claudeSessionId}.jsonl`);
213
+ if (!existsSync2(filePath)) {
214
+ return { messages: [] };
215
+ }
216
+ const messages = [];
217
+ try {
218
+ const content = readFileSync2(filePath, "utf-8");
219
+ const lines = content.split("\n").filter((l) => l.trim());
220
+ for (const line of lines) {
221
+ try {
222
+ const data = JSON.parse(line);
223
+ if (data.type === "user" && data.message?.content) {
224
+ const blocks = Array.isArray(data.message.content) ? data.message.content : [{ type: "text", text: String(data.message.content) }];
225
+ const textParts = [];
226
+ for (const block of blocks) {
227
+ if (block.type === "text" && block.text?.trim()) {
228
+ textParts.push(block.text);
229
+ }
230
+ }
231
+ if (textParts.length > 0) {
232
+ messages.push({ role: "user", content: textParts.join("\n") });
233
+ }
234
+ const lastAssistant = messages.length > 0 ? [...messages].reverse().find((m) => m.role === "assistant" && m.toolCalls?.length) : void 0;
235
+ if (lastAssistant?.toolCalls) {
236
+ for (const block of blocks) {
237
+ if (block.type === "tool_result") {
238
+ const pending = lastAssistant.toolCalls.find((tc) => !tc.output);
239
+ if (pending) {
240
+ pending.output = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((c) => c.text ?? "").join("") : JSON.stringify(block.content);
241
+ pending.status = block.is_error ? "error" : "done";
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ if (data.type === "assistant" && data.message?.content) {
248
+ let text = "";
249
+ const toolCalls = [];
250
+ let thinking = "";
251
+ for (const block of data.message.content) {
252
+ if (block.type === "text") {
253
+ text += block.text;
254
+ } else if (block.type === "tool_use") {
255
+ toolCalls.push({
256
+ name: block.name,
257
+ input: typeof block.input === "string" ? block.input : JSON.stringify(block.input, null, 2),
258
+ status: "done"
259
+ });
260
+ } else if (block.type === "thinking") {
261
+ thinking += block.thinking ?? "";
262
+ }
263
+ }
264
+ messages.push({
265
+ role: "assistant",
266
+ content: text,
267
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
268
+ thinking: thinking || void 0
269
+ });
270
+ }
271
+ } catch {
272
+ }
273
+ }
274
+ } catch {
275
+ }
276
+ return { messages: messages.slice(-50) };
277
+ }
278
+
279
+ // src/hub-connection.ts
280
+ import { createLogger } from "@mclawnet/logger";
281
+ var log = createLogger({ module: "agent" });
282
+ var HubConnection = class {
283
+ ws = null;
284
+ heartbeatTimer = null;
285
+ reconnectTimer = null;
286
+ reconnectDelay;
287
+ destroyed = false;
288
+ authState = "pending";
289
+ hubUrl;
290
+ token;
291
+ hostname;
292
+ version;
293
+ capabilities;
294
+ heartbeatInterval;
295
+ maxReconnectDelay;
296
+ /** Agent ID assigned by Hub after successful auth */
297
+ agentId = null;
298
+ /** Swarm IDs that have been completed/failed — prevents accidental recreation */
299
+ finishedSwarms = /* @__PURE__ */ new Set();
300
+ /** Session manager — set after construction via setSessionManager() */
301
+ sessionManager = null;
302
+ /** Swarm coordinator — set after construction via setSwarmCoordinator() */
303
+ swarmCoordinator = null;
304
+ onMessage;
305
+ onConnectCb;
306
+ onDisconnect;
307
+ onError;
308
+ constructor(opts) {
309
+ this.hubUrl = opts.hubUrl;
310
+ this.token = opts.token;
311
+ this.hostname = opts.hostname ?? osHostname();
312
+ this.version = opts.version;
313
+ this.capabilities = opts.capabilities;
314
+ this.heartbeatInterval = opts.heartbeatInterval ?? HEARTBEAT_INTERVAL_MS;
315
+ this.reconnectDelay = opts.reconnectDelay ?? DEFAULT_RECONNECT_MS;
316
+ this.maxReconnectDelay = opts.maxReconnectDelay ?? MAX_RECONNECT_MS;
317
+ this.onMessage = opts.onMessage;
318
+ this.onConnectCb = opts.onConnect;
319
+ this.onDisconnect = opts.onDisconnect;
320
+ this.onError = opts.onError;
321
+ }
322
+ setSessionManager(manager) {
323
+ this.sessionManager = manager;
324
+ }
325
+ setSwarmCoordinator(coordinator) {
326
+ this.swarmCoordinator = coordinator;
327
+ }
328
+ get readyState() {
329
+ return this.ws?.readyState ?? WebSocket.CLOSED;
330
+ }
331
+ get isConnected() {
332
+ return this.ws?.readyState === WebSocket.OPEN && this.authState === "authenticated";
333
+ }
334
+ connect() {
335
+ if (this.destroyed) return;
336
+ this.cleanup();
337
+ this.authState = "pending";
338
+ this.ws = new WebSocket(this.hubUrl);
339
+ this.ws.on("open", () => {
340
+ this.reconnectDelay = DEFAULT_RECONNECT_MS;
341
+ });
342
+ this.ws.on("message", (raw) => {
343
+ let data;
344
+ try {
345
+ data = JSON.parse(raw.toString());
346
+ } catch {
347
+ return;
348
+ }
349
+ if (this.authState === "pending" && data.type === "auth_required") {
350
+ this.authState = "authenticating";
351
+ this.sendRaw({
352
+ type: "auth",
353
+ token: this.token,
354
+ hostname: this.hostname,
355
+ version: this.version,
356
+ capabilities: this.capabilities
357
+ });
358
+ return;
359
+ }
360
+ if (this.authState === "authenticating" && data.type === "registered") {
361
+ this.authState = "authenticated";
362
+ this.agentId = data.agentId ?? null;
363
+ this.startHeartbeat();
364
+ this.onConnectCb?.(this.agentId);
365
+ this.tryRecoverSwarms();
366
+ return;
367
+ }
368
+ if (this.authState === "authenticated") {
369
+ if (this.handleSessionMessage(data)) return;
370
+ this.onMessage?.(data);
371
+ }
372
+ });
373
+ this.ws.on("close", (code, reason) => {
374
+ this.stopHeartbeat();
375
+ this.authState = "pending";
376
+ this.onDisconnect?.(code, reason.toString());
377
+ if (code === WS_CLOSE_INVALID_TOKEN) {
378
+ log.error("auth failed \u2014 not reconnecting, check your token");
379
+ return;
380
+ }
381
+ this.scheduleReconnect();
382
+ });
383
+ this.ws.on("error", (err) => {
384
+ this.onError?.(err);
385
+ });
386
+ }
387
+ send(data) {
388
+ const msg = data;
389
+ if (msg.type === "swarm.status" && (msg.swarmStatus === "completed" || msg.swarmStatus === "failed")) {
390
+ this.finishedSwarms.add(msg.sessionId);
391
+ }
392
+ return this.sendRaw(data);
393
+ }
394
+ destroy() {
395
+ this.destroyed = true;
396
+ this.cleanup();
397
+ }
398
+ // ── Session message handling ─────────────────────────────────────
399
+ handleSessionMessage(msg) {
400
+ if (msg.type === "fs.list_dir") {
401
+ handleListDir(msg.path).then((result) => {
402
+ this.send({ type: "fs.list_dir_result", requestId: msg.requestId, ...result });
403
+ }).catch((err) => {
404
+ this.send({
405
+ type: "fs.list_dir_result",
406
+ requestId: msg.requestId,
407
+ path: msg.path || "/",
408
+ entries: []
409
+ });
410
+ });
411
+ return true;
412
+ }
413
+ if (msg.type === "list_folders") {
414
+ handleListFolders().then((result) => {
415
+ this.send({ type: "folders_list_result", requestId: msg.requestId, ...result });
416
+ }).catch(() => {
417
+ this.send({ type: "folders_list_result", requestId: msg.requestId, folders: [] });
418
+ });
419
+ return true;
420
+ }
421
+ if (msg.type === "list_history_sessions") {
422
+ handleListHistorySessions(msg.workDir).then((result) => {
423
+ this.send({ type: "history_sessions_result", requestId: msg.requestId, ...result });
424
+ }).catch(() => {
425
+ this.send({
426
+ type: "history_sessions_result",
427
+ requestId: msg.requestId,
428
+ workDir: msg.workDir,
429
+ sessions: []
430
+ });
431
+ });
432
+ return true;
433
+ }
434
+ if (msg.type === "load_session_history") {
435
+ handleLoadSessionHistory(msg.workDir, msg.claudeSessionId).then((result) => {
436
+ this.send({ type: "session_history_result", requestId: msg.requestId, ...result });
437
+ }).catch(() => {
438
+ this.send({ type: "session_history_result", requestId: msg.requestId, messages: [] });
439
+ });
440
+ return true;
441
+ }
442
+ if (msg.type === "list_roles") {
443
+ const roleNames = listRoles();
444
+ const roles = roleNames.map((name) => {
445
+ try {
446
+ const def = loadRole(name);
447
+ return {
448
+ name: def.name,
449
+ displayName: def.shortName || def.name,
450
+ description: def.description || "",
451
+ capabilities: def.capabilities || [],
452
+ promptBody: def.promptBody || ""
453
+ };
454
+ } catch {
455
+ return { name, displayName: name, description: "", capabilities: [], promptBody: "" };
456
+ }
457
+ });
458
+ this.send({
459
+ type: "roles_list_result",
460
+ sessionId: msg.sessionId,
461
+ roles
462
+ });
463
+ return true;
464
+ }
465
+ if (msg.type === "swarm.execute" && this.swarmCoordinator) {
466
+ const { sessionId, content, workDir, targetInstance, crewConfig } = msg;
467
+ if (this.swarmCoordinator.hasSwarm(sessionId)) {
468
+ this.swarmCoordinator.handleUserMessage(sessionId, content, targetInstance).catch((err) => {
469
+ this.send({
470
+ type: "session.error",
471
+ sessionId,
472
+ error: err instanceof Error ? err.message : String(err)
473
+ });
474
+ });
475
+ } else if (this.finishedSwarms.has(sessionId)) {
476
+ log.info({ sessionId }, "swarm.execute ignored: swarm already finished");
477
+ this.send({
478
+ type: "session.error",
479
+ sessionId,
480
+ error: "\u8702\u7FA4\u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u65E0\u6CD5\u7EE7\u7EED\u64CD\u4F5C\u3002\u8BF7\u521B\u5EFA\u65B0\u7684\u8702\u7FA4\u4EFB\u52A1\u3002"
481
+ });
482
+ } else if (crewConfig?.roles) {
483
+ const roles = crewConfig.roles;
484
+ this.swarmCoordinator.create(sessionId, { workDir, roles, task: content }).catch((err) => {
485
+ this.send({
486
+ type: "session.error",
487
+ sessionId,
488
+ error: err instanceof Error ? err.message : String(err)
489
+ });
490
+ });
491
+ } else {
492
+ log.info({ sessionId }, "swarm.execute ignored: swarm not found, no config");
493
+ this.send({
494
+ type: "session.error",
495
+ sessionId,
496
+ error: "\u8702\u7FA4\u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u65E0\u6CD5\u7EE7\u7EED\u64CD\u4F5C\u3002\u8BF7\u521B\u5EFA\u65B0\u7684\u8702\u7FA4\u4EFB\u52A1\u3002"
497
+ });
498
+ }
499
+ return true;
500
+ }
501
+ if (!this.sessionManager) return false;
502
+ if (msg.type === "abort_execution") {
503
+ const { sessionId } = msg;
504
+ if (this.sessionManager?.hasSession(sessionId)) {
505
+ this.sessionManager.abortSession(sessionId).then(() => {
506
+ this.send({ type: "execution_aborted", sessionId });
507
+ }).catch(() => {
508
+ this.send({ type: "execution_aborted", sessionId });
509
+ });
510
+ } else {
511
+ this.send({ type: "execution_aborted", sessionId });
512
+ }
513
+ return true;
514
+ }
515
+ if (msg.type === "claude.execute") {
516
+ const { sessionId, content, workDir, claudeSessionId, useBrainCore } = msg;
517
+ if (this.sessionManager.hasSession(sessionId)) {
518
+ this.sessionManager.sendInput(sessionId, content);
519
+ } else {
520
+ this.sessionManager.createSession({ sessionId, workDir, resumeId: claudeSessionId, useBrainCore }).then(() => {
521
+ this.sessionManager.sendInput(sessionId, content);
522
+ }).catch((err) => {
523
+ this.send({
524
+ type: "session.error",
525
+ sessionId,
526
+ error: err instanceof Error ? err.message : String(err)
527
+ });
528
+ });
529
+ }
530
+ return true;
531
+ }
532
+ if (msg.type === "session.create") {
533
+ this.sessionManager.createSession({
534
+ sessionId: msg.sessionId,
535
+ workDir: msg.workDir,
536
+ resumeId: msg.resumeId
537
+ }).then((claudeSessionId) => {
538
+ this.send({
539
+ type: "session.created",
540
+ sessionId: msg.sessionId,
541
+ claudeSessionId
542
+ });
543
+ }).catch((err) => {
544
+ this.send({
545
+ type: "session.error",
546
+ sessionId: msg.sessionId,
547
+ error: err instanceof Error ? err.message : String(err)
548
+ });
549
+ });
550
+ return true;
551
+ }
552
+ if (msg.type === "session.close") {
553
+ this.sessionManager.closeSession(msg.sessionId).catch(() => {
554
+ });
555
+ return true;
556
+ }
557
+ if (msg.type === "claude.input") {
558
+ this.sessionManager.sendInput(msg.sessionId, msg.content);
559
+ return true;
560
+ }
561
+ return false;
562
+ }
563
+ // ── Internal helpers ─────────────────────────────────────────────
564
+ tryRecoverSwarms() {
565
+ if (!this.swarmCoordinator) return;
566
+ try {
567
+ const allIds = listRecoverableSwarmIds();
568
+ const snapshots = listRecoverableSwarms();
569
+ const recoverableIds = new Set(snapshots.map((s) => s.id));
570
+ for (const id of allIds) {
571
+ if (!recoverableIds.has(id)) {
572
+ deleteSwarmSnapshot(id);
573
+ log.info({ swarmId: id }, "cleaned up non-recoverable swarm snapshot");
574
+ }
575
+ }
576
+ for (const snap of snapshots) {
577
+ log.info({ swarmId: snap.id }, "recovering swarm");
578
+ recoverSwarm(this.swarmCoordinator, snap).catch((err) => {
579
+ log.error({ err, swarmId: snap.id }, "failed to recover swarm");
580
+ });
581
+ }
582
+ } catch (err) {
583
+ log.error({ err }, "swarm recovery failed");
584
+ }
585
+ }
586
+ sendRaw(data) {
587
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return false;
588
+ this.ws.send(JSON.stringify(data));
589
+ return true;
590
+ }
591
+ startHeartbeat() {
592
+ this.stopHeartbeat();
593
+ this.heartbeatTimer = setInterval(() => {
594
+ this.send({ type: "heartbeat", ts: Date.now() });
595
+ }, this.heartbeatInterval);
596
+ }
597
+ stopHeartbeat() {
598
+ if (this.heartbeatTimer) {
599
+ clearInterval(this.heartbeatTimer);
600
+ this.heartbeatTimer = null;
601
+ }
602
+ }
603
+ scheduleReconnect() {
604
+ if (this.destroyed) return;
605
+ this.reconnectTimer = setTimeout(() => {
606
+ this.connect();
607
+ }, this.reconnectDelay);
608
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
609
+ }
610
+ cleanup() {
611
+ this.stopHeartbeat();
612
+ this.sessionManager?.closeAll().catch(() => {
613
+ });
614
+ if (this.reconnectTimer) {
615
+ clearTimeout(this.reconnectTimer);
616
+ this.reconnectTimer = null;
617
+ }
618
+ if (this.ws) {
619
+ this.ws.removeAllListeners();
620
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
621
+ this.ws.close();
622
+ }
623
+ this.ws = null;
624
+ }
625
+ }
626
+ };
627
+
628
+ // src/session-manager.ts
629
+ var SessionManager = class {
630
+ sessions = /* @__PURE__ */ new Map();
631
+ adapter;
632
+ onOutput;
633
+ onTurnComplete;
634
+ onSessionError;
635
+ constructor(options) {
636
+ this.adapter = options.adapter;
637
+ this.onOutput = options.onOutput;
638
+ this.onTurnComplete = options.onTurnComplete;
639
+ this.onSessionError = options.onSessionError;
640
+ }
641
+ async createSession(options) {
642
+ if (this.sessions.has(options.sessionId)) {
643
+ throw new Error(`Session ${options.sessionId} already exists`);
644
+ }
645
+ try {
646
+ const process2 = await this.adapter.spawn(options);
647
+ this.sessions.set(options.sessionId, process2);
648
+ this.adapter.onOutput(process2, (data) => {
649
+ this.onOutput(options.sessionId, data);
650
+ });
651
+ this.adapter.onTurnComplete?.(process2, (info) => {
652
+ this.onTurnComplete(options.sessionId, info);
653
+ });
654
+ this.adapter.onError?.(process2, (error) => {
655
+ this.onSessionError(options.sessionId, error.message);
656
+ });
657
+ return process2.id;
658
+ } catch (err) {
659
+ const message = err instanceof Error ? err.message : String(err);
660
+ this.onSessionError(options.sessionId, message);
661
+ throw err;
662
+ }
663
+ }
664
+ sendInput(sessionId, input) {
665
+ const process2 = this.sessions.get(sessionId);
666
+ if (!process2) {
667
+ this.onSessionError(sessionId, `No active session: ${sessionId}`);
668
+ return;
669
+ }
670
+ this.adapter.send(process2, input);
671
+ }
672
+ async abortSession(sessionId) {
673
+ const process2 = this.sessions.get(sessionId);
674
+ if (!process2) return;
675
+ this.sessions.delete(sessionId);
676
+ await this.adapter.stop(process2);
677
+ }
678
+ async closeSession(sessionId) {
679
+ const process2 = this.sessions.get(sessionId);
680
+ if (!process2) return;
681
+ this.sessions.delete(sessionId);
682
+ await this.adapter.stop(process2);
683
+ }
684
+ async closeAll() {
685
+ const promises = Array.from(this.sessions.entries()).map(
686
+ async ([sessionId, process2]) => {
687
+ this.sessions.delete(sessionId);
688
+ await this.adapter.stop(process2).catch(() => {
689
+ });
690
+ }
691
+ );
692
+ await Promise.all(promises);
693
+ }
694
+ hasSession(sessionId) {
695
+ return this.sessions.has(sessionId);
696
+ }
697
+ get activeSessionCount() {
698
+ return this.sessions.size;
699
+ }
700
+ };
701
+
702
+ // src/start.ts
703
+ import { SwarmCoordinator, initRoles } from "@mclawnet/swarm";
704
+ import { createLogger as createLogger2 } from "@mclawnet/logger";
705
+ var log2 = createLogger2({ module: "agent" });
706
+ async function startAgent(options) {
707
+ const config = loadConfig(options.config);
708
+ if (!config.token) {
709
+ log2.error("no token configured \u2014 set CLAWNET_TOKEN or use --token");
710
+ process.exit(1);
711
+ }
712
+ log2.info({ backend: options.adapter.type }, "starting agent");
713
+ log2.info({ hubUrl: config.hubUrl }, "connecting to hub");
714
+ await initRoles();
715
+ const hub = new HubConnection({
716
+ hubUrl: config.hubUrl,
717
+ token: config.token,
718
+ hostname: config.name,
719
+ onConnect: (agentId) => {
720
+ log2.info({ agentId }, "connected to hub");
721
+ },
722
+ onDisconnect: (code, reason) => {
723
+ log2.info({ code, reason }, "disconnected from hub");
724
+ },
725
+ onError: (err) => {
726
+ log2.error({ err }, "hub connection error");
727
+ }
728
+ });
729
+ let swarmCoordinator;
730
+ const sessionManager = new SessionManager({
731
+ adapter: options.adapter,
732
+ onOutput: (sessionId, data) => {
733
+ if (swarmCoordinator.handleRoleOutput(sessionId, data)) return;
734
+ hub.send({
735
+ type: "claude.output",
736
+ sessionId,
737
+ data
738
+ });
739
+ },
740
+ onTurnComplete: (sessionId, info) => {
741
+ if (swarmCoordinator.handleRoleTurnComplete(sessionId, info)) return;
742
+ hub.send({
743
+ type: "claude.turn_complete",
744
+ sessionId,
745
+ cost: info.cost,
746
+ duration: info.duration,
747
+ contextUsage: info.contextUsage
748
+ });
749
+ },
750
+ onSessionError: (sessionId, error) => {
751
+ hub.send({
752
+ type: "session.error",
753
+ sessionId,
754
+ error
755
+ });
756
+ }
757
+ });
758
+ swarmCoordinator = new SwarmCoordinator(sessionManager, hub);
759
+ hub.setSessionManager(sessionManager);
760
+ hub.setSwarmCoordinator(swarmCoordinator);
761
+ const shutdown = async () => {
762
+ log2.info("shutting down");
763
+ await sessionManager.closeAll();
764
+ hub.destroy();
765
+ process.exit(0);
766
+ };
767
+ process.on("SIGINT", shutdown);
768
+ process.on("SIGTERM", shutdown);
769
+ hub.connect();
770
+ return { hub, sessionManager, swarmCoordinator };
771
+ }
772
+
773
+ export {
774
+ HubConnection,
775
+ SessionManager,
776
+ loadConfig,
777
+ saveConfig,
778
+ startAgent
779
+ };
780
+ //# sourceMappingURL=chunk-YBQQZNRQ.js.map