@esotech/contextuate 2.0.0 → 2.1.0
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/README.md +169 -1
- package/dist/commands/claude.d.ts +21 -0
- package/dist/commands/claude.js +213 -0
- package/dist/commands/context.d.ts +1 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +67 -6
- package/dist/commands/install.d.ts +28 -0
- package/dist/commands/install.js +116 -11
- package/dist/commands/monitor.d.ts +55 -0
- package/dist/commands/monitor.js +1007 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/run.d.ts +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +113 -1
- package/dist/monitor/daemon/circuit-breaker.d.ts +121 -0
- package/dist/monitor/daemon/circuit-breaker.js +552 -0
- package/dist/monitor/daemon/cli.d.ts +8 -0
- package/dist/monitor/daemon/cli.js +82 -0
- package/dist/monitor/daemon/index.d.ts +137 -0
- package/dist/monitor/daemon/index.js +695 -0
- package/dist/monitor/daemon/notifier.d.ts +25 -0
- package/dist/monitor/daemon/notifier.js +98 -0
- package/dist/monitor/daemon/processor.d.ts +89 -0
- package/dist/monitor/daemon/processor.js +455 -0
- package/dist/monitor/daemon/state.d.ts +80 -0
- package/dist/monitor/daemon/state.js +162 -0
- package/dist/monitor/daemon/watcher.d.ts +47 -0
- package/dist/monitor/daemon/watcher.js +171 -0
- package/dist/monitor/daemon/wrapper-manager.d.ts +106 -0
- package/dist/monitor/daemon/wrapper-manager.js +374 -0
- package/dist/monitor/hooks/emit-event.js +652 -0
- package/dist/monitor/persistence/file-store.d.ts +88 -0
- package/dist/monitor/persistence/file-store.js +335 -0
- package/dist/monitor/persistence/index.d.ts +7 -0
- package/dist/monitor/persistence/index.js +10 -0
- package/dist/monitor/server/adapters/redis.d.ts +38 -0
- package/dist/monitor/server/adapters/redis.js +213 -0
- package/dist/monitor/server/adapters/unix-socket.d.ts +33 -0
- package/dist/monitor/server/adapters/unix-socket.js +182 -0
- package/dist/monitor/server/broker.d.ts +135 -0
- package/dist/monitor/server/broker.js +475 -0
- package/dist/monitor/server/cli.d.ts +8 -0
- package/dist/monitor/server/cli.js +98 -0
- package/dist/monitor/server/fastify.d.ts +16 -0
- package/dist/monitor/server/fastify.js +184 -0
- package/dist/monitor/server/index.d.ts +36 -0
- package/dist/monitor/server/index.js +153 -0
- package/dist/monitor/server/websocket.d.ts +80 -0
- package/dist/monitor/server/websocket.js +453 -0
- package/dist/monitor/ui/assets/index-4IssW9On.js +59 -0
- package/dist/monitor/ui/assets/index-vo9hLe5R.css +32 -0
- package/dist/monitor/ui/favicon.png +0 -0
- package/dist/monitor/ui/index.html +14 -0
- package/dist/monitor/ui/logo.png +0 -0
- package/dist/monitor/ui/logo.svg +1 -0
- package/dist/runtime/driver.d.ts +16 -0
- package/dist/runtime/tools.d.ts +10 -0
- package/dist/templates/README.md +33 -7
- package/dist/templates/agents/aegis.md +4 -0
- package/dist/templates/agents/archon.md +13 -22
- package/dist/templates/agents/atlas.md +4 -0
- package/dist/templates/agents/canvas.md +4 -0
- package/dist/templates/agents/chronicle.md +4 -0
- package/dist/templates/agents/chronos.md +4 -0
- package/dist/templates/agents/cipher.md +4 -0
- package/dist/templates/agents/crucible.md +4 -0
- package/dist/templates/agents/echo.md +4 -0
- package/dist/templates/agents/forge.md +4 -0
- package/dist/templates/agents/ledger.md +4 -0
- package/dist/templates/agents/meridian.md +4 -0
- package/dist/templates/agents/nexus.md +4 -0
- package/dist/templates/agents/pythia.md +217 -0
- package/dist/templates/agents/scribe.md +4 -0
- package/dist/templates/agents/sentinel.md +4 -0
- package/dist/templates/agents/{oracle.md → thoth.md} +11 -7
- package/dist/templates/agents/unity.md +4 -0
- package/dist/templates/agents/vox.md +4 -0
- package/dist/templates/agents/weaver.md +4 -0
- package/dist/templates/framework-agents/documentation-expert.md +3 -3
- package/dist/templates/framework-agents/tools-expert.md +8 -8
- package/dist/templates/skills/consult.md +138 -0
- package/dist/templates/skills/orchestrate.md +173 -0
- package/dist/templates/skills/pythia.md +37 -0
- package/dist/templates/standards/agent-roles.md +68 -21
- package/dist/templates/standards/coding-standards.md +9 -26
- package/dist/templates/templates/context.md +17 -2
- package/dist/templates/templates/contextuate.md +21 -28
- package/dist/templates/templates/standards/go.md +167 -0
- package/dist/templates/templates/standards/java.md +167 -0
- package/dist/templates/templates/standards/javascript.md +292 -0
- package/dist/templates/templates/standards/php.md +181 -0
- package/dist/templates/templates/standards/python.md +175 -0
- package/dist/templates/tools/agent-creator.md +252 -0
- package/dist/templates/tools/agent-creator.tool.md +2 -2
- package/dist/templates/tools/quickref.md +216 -0
- package/dist/templates/tools/spawn.md +31 -0
- package/dist/templates/tools/standards-detector.md +301 -0
- package/dist/templates/version.json +1 -1
- package/dist/types/monitor.d.ts +660 -0
- package/dist/types/monitor.js +75 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/tokens.d.ts +10 -0
- package/package.json +18 -5
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Monitor Daemon
|
|
4
|
+
*
|
|
5
|
+
* Background daemon process that:
|
|
6
|
+
* - Watches raw/ directory for new events
|
|
7
|
+
* - Processes events (correlates sessions, tracks subagents)
|
|
8
|
+
* - Persists data to sessions/
|
|
9
|
+
* - Notifies UI server via Unix socket/Redis
|
|
10
|
+
* - Manages Claude wrapper sessions with PTY
|
|
11
|
+
*
|
|
12
|
+
* This is Layer 2 of the 3-layer architecture:
|
|
13
|
+
* Layer 1: Hooks (write to raw/)
|
|
14
|
+
* Layer 2: Daemon (process raw/ -> sessions/)
|
|
15
|
+
* Layer 3: UI Server (read from sessions/, serve WebSocket)
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.MonitorDaemon = void 0;
|
|
52
|
+
exports.startDaemon = startDaemon;
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const net = __importStar(require("net"));
|
|
56
|
+
const monitor_js_1 = require("../../types/monitor.js");
|
|
57
|
+
const state_js_1 = require("./state.js");
|
|
58
|
+
const watcher_js_1 = require("./watcher.js");
|
|
59
|
+
const processor_js_1 = require("./processor.js");
|
|
60
|
+
const notifier_js_1 = require("./notifier.js");
|
|
61
|
+
const wrapper_manager_js_1 = require("./wrapper-manager.js");
|
|
62
|
+
const circuit_breaker_js_1 = require("./circuit-breaker.js");
|
|
63
|
+
const PATHS = (0, monitor_js_1.getDefaultMonitorPaths)();
|
|
64
|
+
class MonitorDaemon {
|
|
65
|
+
constructor(config, circuitBreakerConfig) {
|
|
66
|
+
this.socketServer = null;
|
|
67
|
+
this.uiClients = new Set();
|
|
68
|
+
this.legacyWrapperSessions = new Map();
|
|
69
|
+
this.running = false;
|
|
70
|
+
this.config = config;
|
|
71
|
+
this.state = new state_js_1.StateManager();
|
|
72
|
+
this.notifier = new notifier_js_1.Notifier(config, (data) => this.broadcastToClients(data));
|
|
73
|
+
this.processor = new processor_js_1.EventProcessor(this.state, this.notifier);
|
|
74
|
+
this.watcher = new watcher_js_1.FileWatcher();
|
|
75
|
+
// Initialize wrapper manager
|
|
76
|
+
const wrapperPersistPath = path.join(PATHS.baseDir, 'wrappers.json');
|
|
77
|
+
this.wrapperManager = new wrapper_manager_js_1.WrapperManager(wrapperPersistPath, (event) => {
|
|
78
|
+
this.handleWrapperManagerEvent(event);
|
|
79
|
+
});
|
|
80
|
+
// Initialize circuit breaker
|
|
81
|
+
this.circuitBreaker = new circuit_breaker_js_1.CircuitBreaker(circuitBreakerConfig || {}, this.wrapperManager, (alert) => this.handleCircuitAlert(alert));
|
|
82
|
+
// Connect event processor to circuit breaker
|
|
83
|
+
this.processor.setCircuitBreaker(this.circuitBreaker);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Handle circuit breaker alerts
|
|
87
|
+
*/
|
|
88
|
+
handleCircuitAlert(alert) {
|
|
89
|
+
console.log(`[Daemon] Circuit alert: ${alert.sessionId} ${alert.previousState} → ${alert.newState} (${alert.reason})`);
|
|
90
|
+
this.broadcastToClients({
|
|
91
|
+
type: 'circuit_alert',
|
|
92
|
+
alert,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Handle events from WrapperManager
|
|
97
|
+
*/
|
|
98
|
+
handleWrapperManagerEvent(event) {
|
|
99
|
+
switch (event.type) {
|
|
100
|
+
case 'output':
|
|
101
|
+
this.broadcastToClients({
|
|
102
|
+
type: 'wrapper_output',
|
|
103
|
+
wrapperId: event.wrapperId,
|
|
104
|
+
data: event.data,
|
|
105
|
+
timestamp: Date.now(),
|
|
106
|
+
});
|
|
107
|
+
break;
|
|
108
|
+
case 'state_changed':
|
|
109
|
+
this.broadcastToClients({
|
|
110
|
+
type: 'wrapper_state',
|
|
111
|
+
wrapperId: event.wrapperId,
|
|
112
|
+
state: event.state,
|
|
113
|
+
claudeSessionId: event.claudeSessionId,
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
case 'started':
|
|
117
|
+
this.broadcastToClients({
|
|
118
|
+
type: 'wrapper_connected',
|
|
119
|
+
wrapperId: event.wrapperId,
|
|
120
|
+
state: event.state || 'starting',
|
|
121
|
+
});
|
|
122
|
+
break;
|
|
123
|
+
case 'ended':
|
|
124
|
+
this.broadcastToClients({
|
|
125
|
+
type: 'wrapper_disconnected',
|
|
126
|
+
wrapperId: event.wrapperId,
|
|
127
|
+
exitCode: event.exitCode,
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Broadcast data to all connected UI clients
|
|
134
|
+
*/
|
|
135
|
+
broadcastToClients(data) {
|
|
136
|
+
const message = JSON.stringify(data) + '\n';
|
|
137
|
+
for (const client of this.uiClients) {
|
|
138
|
+
try {
|
|
139
|
+
client.write(message);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// Client disconnected, will be cleaned up
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Start the daemon
|
|
148
|
+
*/
|
|
149
|
+
async start() {
|
|
150
|
+
console.log('[Daemon] Starting monitor daemon...');
|
|
151
|
+
// Load state from disk
|
|
152
|
+
await this.state.load();
|
|
153
|
+
// Load existing sessions
|
|
154
|
+
await this.processor.loadSessions();
|
|
155
|
+
// Initialize wrapper manager (loads persisted wrappers, cleans up dead ones)
|
|
156
|
+
await this.wrapperManager.initialize();
|
|
157
|
+
// Set up file watcher
|
|
158
|
+
this.watcher.setLastProcessedTimestamp(this.state.lastProcessedTimestamp);
|
|
159
|
+
this.watcher.setHandler((event, filepath) => this.processor.processEvent(event, filepath));
|
|
160
|
+
await this.watcher.start();
|
|
161
|
+
// Start periodic state saving (every 30 seconds)
|
|
162
|
+
this.state.startPeriodicSave(30000);
|
|
163
|
+
// Start socket server for UI connections
|
|
164
|
+
await this.startSocketServer();
|
|
165
|
+
// Start circuit breaker health monitoring
|
|
166
|
+
this.circuitBreaker.start();
|
|
167
|
+
this.running = true;
|
|
168
|
+
console.log('[Daemon] Monitor daemon started');
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Stop the daemon
|
|
172
|
+
*/
|
|
173
|
+
async stop() {
|
|
174
|
+
console.log('[Daemon] Stopping monitor daemon...');
|
|
175
|
+
this.running = false;
|
|
176
|
+
// Stop circuit breaker health monitoring
|
|
177
|
+
this.circuitBreaker.stop();
|
|
178
|
+
// Shutdown wrapper manager (kills all wrappers, persists state)
|
|
179
|
+
await this.wrapperManager.shutdown();
|
|
180
|
+
// Stop file watcher
|
|
181
|
+
await this.watcher.stop();
|
|
182
|
+
// Stop periodic save and save final state
|
|
183
|
+
this.state.stopPeriodicSave();
|
|
184
|
+
await this.state.save();
|
|
185
|
+
// Close socket server
|
|
186
|
+
if (this.socketServer) {
|
|
187
|
+
// Forcefully disconnect all UI clients
|
|
188
|
+
for (const client of this.uiClients) {
|
|
189
|
+
try {
|
|
190
|
+
client.write(JSON.stringify({ type: 'error', message: 'Daemon shutting down' }) + '\n');
|
|
191
|
+
client.destroy(); // Force close the connection
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
// Ignore errors during shutdown
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
await new Promise((resolve) => {
|
|
198
|
+
// Add timeout to force resolve if close takes too long
|
|
199
|
+
const timeout = setTimeout(() => {
|
|
200
|
+
console.log('[Daemon] Socket server close timeout, forcing shutdown');
|
|
201
|
+
this.socketServer = null;
|
|
202
|
+
this.uiClients.clear();
|
|
203
|
+
// Clean up socket file
|
|
204
|
+
try {
|
|
205
|
+
fs.unlinkSync('/tmp/contextuate-daemon.sock');
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
// Ignore if already removed
|
|
209
|
+
}
|
|
210
|
+
resolve();
|
|
211
|
+
}, 3000);
|
|
212
|
+
this.socketServer.close(() => {
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
this.socketServer = null;
|
|
215
|
+
this.uiClients.clear();
|
|
216
|
+
console.log('[Daemon] Socket server stopped');
|
|
217
|
+
// Clean up socket file
|
|
218
|
+
try {
|
|
219
|
+
fs.unlinkSync('/tmp/contextuate-daemon.sock');
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
// Ignore if already removed
|
|
223
|
+
}
|
|
224
|
+
resolve();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
console.log('[Daemon] Monitor daemon stopped');
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if daemon is running
|
|
232
|
+
*/
|
|
233
|
+
isRunning() {
|
|
234
|
+
return this.running;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Start Unix socket server for hook notifications and UI clients
|
|
238
|
+
*/
|
|
239
|
+
async startSocketServer() {
|
|
240
|
+
const socketPath = '/tmp/contextuate-daemon.sock';
|
|
241
|
+
// Remove existing socket file
|
|
242
|
+
try {
|
|
243
|
+
await fs.promises.unlink(socketPath);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
// Ignore if doesn't exist
|
|
247
|
+
}
|
|
248
|
+
this.socketServer = net.createServer((socket) => {
|
|
249
|
+
// Track this as a UI client for broadcasting
|
|
250
|
+
this.uiClients.add(socket);
|
|
251
|
+
console.log(`[Daemon] Client connected (${this.uiClients.size} total)`);
|
|
252
|
+
// Handle incoming data (from hooks, wrappers, or UI commands)
|
|
253
|
+
let buffer = '';
|
|
254
|
+
let isWrapper = false;
|
|
255
|
+
let wrapperId = null;
|
|
256
|
+
socket.on('data', (data) => {
|
|
257
|
+
buffer += data.toString();
|
|
258
|
+
const lines = buffer.split('\n');
|
|
259
|
+
buffer = lines.pop() || '';
|
|
260
|
+
for (const line of lines) {
|
|
261
|
+
if (line.trim()) {
|
|
262
|
+
try {
|
|
263
|
+
const message = JSON.parse(line);
|
|
264
|
+
// Handle wrapper registration
|
|
265
|
+
if (message.type === 'wrapper_register') {
|
|
266
|
+
isWrapper = true;
|
|
267
|
+
wrapperId = message.wrapperId;
|
|
268
|
+
this.handleWrapperRegister(socket, message);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
// Handle wrapper messages
|
|
272
|
+
if (message.type === 'wrapper_started') {
|
|
273
|
+
this.handleWrapperStarted(message);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (message.type === 'wrapper_ended') {
|
|
277
|
+
this.handleWrapperEnded(message);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (message.type === 'state_changed') {
|
|
281
|
+
this.handleWrapperStateChange(message);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (message.type === 'output') {
|
|
285
|
+
this.handleWrapperOutput(message);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
// Handle input injection request from UI
|
|
289
|
+
if (message.type === 'inject_input') {
|
|
290
|
+
this.handleInputInjection(message);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// Handle resize request from UI
|
|
294
|
+
if (message.type === 'resize_wrapper') {
|
|
295
|
+
this.handleWrapperResize(message);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
// Handle spawn wrapper request
|
|
299
|
+
if (message.type === 'spawn_wrapper') {
|
|
300
|
+
this.handleSpawnWrapper(socket, message);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// Handle kill wrapper request
|
|
304
|
+
if (message.type === 'kill_wrapper') {
|
|
305
|
+
this.handleKillWrapper(message);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
// Handle get wrappers request
|
|
309
|
+
if (message.type === 'get_wrappers') {
|
|
310
|
+
this.handleGetWrappers(socket);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// Circuit breaker messages
|
|
314
|
+
if (message.type === 'get_circuit_health') {
|
|
315
|
+
this.handleGetCircuitHealth(socket);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (message.type === 'get_session_health') {
|
|
319
|
+
this.handleGetSessionHealth(socket, message);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (message.type === 'reset_circuit') {
|
|
323
|
+
this.handleResetCircuit(message);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (message.type === 'update_circuit_config') {
|
|
327
|
+
this.handleUpdateCircuitConfig(socket, message);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
// Process hook events immediately for instant updates
|
|
331
|
+
if (message.id && message.eventType) {
|
|
332
|
+
// This is an event from a hook - process it instantly
|
|
333
|
+
this.processor.processEvent(message, null).catch(err => {
|
|
334
|
+
console.error('[Daemon] Error processing socket event:', err);
|
|
335
|
+
});
|
|
336
|
+
// Check if this event indicates waiting for input
|
|
337
|
+
// and notify relevant wrapper
|
|
338
|
+
this.checkAndNotifyWrapperState(message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
// Ignore parse errors
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
socket.on('close', () => {
|
|
348
|
+
this.uiClients.delete(socket);
|
|
349
|
+
// Clean up legacy wrapper session if this was an external wrapper
|
|
350
|
+
if (isWrapper && wrapperId) {
|
|
351
|
+
this.legacyWrapperSessions.delete(wrapperId);
|
|
352
|
+
console.log(`[Daemon] Legacy wrapper ${wrapperId} disconnected (${this.legacyWrapperSessions.size} legacy wrappers remaining)`);
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
console.log(`[Daemon] Client disconnected (${this.uiClients.size} remaining)`);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
socket.on('error', () => {
|
|
359
|
+
this.uiClients.delete(socket);
|
|
360
|
+
if (isWrapper && wrapperId) {
|
|
361
|
+
this.legacyWrapperSessions.delete(wrapperId);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
this.socketServer.listen(socketPath, () => {
|
|
366
|
+
console.log(`[Daemon] Socket server listening on ${socketPath}`);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Handle wrapper registration (legacy external wrapper process)
|
|
371
|
+
*/
|
|
372
|
+
handleWrapperRegister(socket, message) {
|
|
373
|
+
const session = {
|
|
374
|
+
wrapperId: message.wrapperId,
|
|
375
|
+
socket,
|
|
376
|
+
pid: message.pid,
|
|
377
|
+
claudeSessionId: null,
|
|
378
|
+
state: 'starting',
|
|
379
|
+
cwd: '',
|
|
380
|
+
startTime: Date.now(),
|
|
381
|
+
};
|
|
382
|
+
this.legacyWrapperSessions.set(message.wrapperId, session);
|
|
383
|
+
console.log(`[Daemon] Legacy wrapper registered: ${message.wrapperId} (PID: ${message.pid})`);
|
|
384
|
+
// Acknowledge registration
|
|
385
|
+
socket.write(JSON.stringify({ type: 'registered', wrapperId: message.wrapperId }) + '\n');
|
|
386
|
+
// Notify UI clients about new wrapper
|
|
387
|
+
this.broadcastToClients({
|
|
388
|
+
type: 'wrapper_connected',
|
|
389
|
+
wrapperId: message.wrapperId,
|
|
390
|
+
state: 'starting',
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Handle wrapper started notification (legacy)
|
|
395
|
+
*/
|
|
396
|
+
handleWrapperStarted(message) {
|
|
397
|
+
const session = this.legacyWrapperSessions.get(message.wrapperId);
|
|
398
|
+
if (session) {
|
|
399
|
+
session.cwd = message.cwd || '';
|
|
400
|
+
console.log(`[Daemon] Legacy wrapper ${message.wrapperId} started Claude in ${session.cwd}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Handle wrapper ended notification (legacy)
|
|
405
|
+
*/
|
|
406
|
+
handleWrapperEnded(message) {
|
|
407
|
+
const session = this.legacyWrapperSessions.get(message.wrapperId);
|
|
408
|
+
if (session) {
|
|
409
|
+
session.state = 'ended';
|
|
410
|
+
console.log(`[Daemon] Legacy wrapper ${message.wrapperId} ended (exit: ${message.exitCode})`);
|
|
411
|
+
// Notify UI clients
|
|
412
|
+
this.broadcastToClients({
|
|
413
|
+
type: 'wrapper_disconnected',
|
|
414
|
+
wrapperId: message.wrapperId,
|
|
415
|
+
exitCode: message.exitCode,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Handle wrapper state change (legacy)
|
|
421
|
+
*/
|
|
422
|
+
handleWrapperStateChange(message) {
|
|
423
|
+
const session = this.legacyWrapperSessions.get(message.wrapperId);
|
|
424
|
+
if (session) {
|
|
425
|
+
session.state = message.state;
|
|
426
|
+
console.log(`[Daemon] Legacy wrapper ${message.wrapperId} state: ${message.state}`);
|
|
427
|
+
// Notify UI clients about state change
|
|
428
|
+
this.broadcastToClients({
|
|
429
|
+
type: 'wrapper_state',
|
|
430
|
+
wrapperId: message.wrapperId,
|
|
431
|
+
state: message.state,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Handle wrapper output (for legacy external wrappers)
|
|
437
|
+
*/
|
|
438
|
+
handleWrapperOutput(message) {
|
|
439
|
+
// Forward output to UI clients for session log view
|
|
440
|
+
this.broadcastToClients({
|
|
441
|
+
type: 'wrapper_output',
|
|
442
|
+
wrapperId: message.wrapperId,
|
|
443
|
+
data: message.data,
|
|
444
|
+
timestamp: message.timestamp,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Handle input injection request from UI
|
|
449
|
+
*/
|
|
450
|
+
handleInputInjection(message) {
|
|
451
|
+
const { wrapperId, input } = message;
|
|
452
|
+
// Try managed wrapper first
|
|
453
|
+
if (this.wrapperManager.writeInput(wrapperId, input)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
// Fall back to legacy wrapper
|
|
457
|
+
const session = this.legacyWrapperSessions.get(wrapperId);
|
|
458
|
+
if (session) {
|
|
459
|
+
session.socket.write(JSON.stringify({
|
|
460
|
+
type: 'inject_input',
|
|
461
|
+
input,
|
|
462
|
+
}) + '\n');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Handle resize request from UI
|
|
467
|
+
*/
|
|
468
|
+
handleWrapperResize(message) {
|
|
469
|
+
const { wrapperId, cols, rows } = message;
|
|
470
|
+
// Try managed wrapper first
|
|
471
|
+
if (this.wrapperManager.resize(wrapperId, cols, rows)) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
// Fall back to legacy wrapper
|
|
475
|
+
const session = this.legacyWrapperSessions.get(wrapperId);
|
|
476
|
+
if (session) {
|
|
477
|
+
session.socket.write(JSON.stringify({
|
|
478
|
+
type: 'resize',
|
|
479
|
+
cols,
|
|
480
|
+
rows,
|
|
481
|
+
}) + '\n');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Handle spawn wrapper request
|
|
486
|
+
*/
|
|
487
|
+
async handleSpawnWrapper(socket, message) {
|
|
488
|
+
try {
|
|
489
|
+
const wrapperId = await this.wrapperManager.spawn({
|
|
490
|
+
cwd: message.cwd || process.cwd(),
|
|
491
|
+
args: message.args || [],
|
|
492
|
+
cols: message.cols || 120,
|
|
493
|
+
rows: message.rows || 40,
|
|
494
|
+
});
|
|
495
|
+
// Send response with wrapper ID
|
|
496
|
+
socket.write(JSON.stringify({
|
|
497
|
+
type: 'wrapper_spawned',
|
|
498
|
+
wrapperId,
|
|
499
|
+
success: true,
|
|
500
|
+
}) + '\n');
|
|
501
|
+
console.log(`[Daemon] Spawned wrapper ${wrapperId}`);
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
const error = err;
|
|
505
|
+
socket.write(JSON.stringify({
|
|
506
|
+
type: 'wrapper_spawned',
|
|
507
|
+
success: false,
|
|
508
|
+
error: error.message,
|
|
509
|
+
}) + '\n');
|
|
510
|
+
console.error(`[Daemon] Failed to spawn wrapper:`, error.message);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Handle kill wrapper request
|
|
515
|
+
*/
|
|
516
|
+
handleKillWrapper(message) {
|
|
517
|
+
const { wrapperId } = message;
|
|
518
|
+
if (this.wrapperManager.kill(wrapperId)) {
|
|
519
|
+
console.log(`[Daemon] Killed wrapper ${wrapperId}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Handle get wrappers request
|
|
524
|
+
*/
|
|
525
|
+
handleGetWrappers(socket) {
|
|
526
|
+
const managedWrappers = this.wrapperManager.getAll().map(w => ({
|
|
527
|
+
wrapperId: w.wrapperId,
|
|
528
|
+
pid: w.pid,
|
|
529
|
+
state: w.state,
|
|
530
|
+
claudeSessionId: w.claudeSessionId,
|
|
531
|
+
cwd: w.cwd,
|
|
532
|
+
startTime: w.startTime,
|
|
533
|
+
managed: true,
|
|
534
|
+
}));
|
|
535
|
+
const legacyWrappers = Array.from(this.legacyWrapperSessions.values()).map(w => ({
|
|
536
|
+
wrapperId: w.wrapperId,
|
|
537
|
+
pid: w.pid,
|
|
538
|
+
state: w.state,
|
|
539
|
+
claudeSessionId: w.claudeSessionId,
|
|
540
|
+
cwd: w.cwd,
|
|
541
|
+
startTime: w.startTime,
|
|
542
|
+
managed: false,
|
|
543
|
+
}));
|
|
544
|
+
socket.write(JSON.stringify({
|
|
545
|
+
type: 'wrappers_list',
|
|
546
|
+
wrappers: [...managedWrappers, ...legacyWrappers],
|
|
547
|
+
}) + '\n');
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Check if a hook event indicates waiting for input
|
|
551
|
+
* and notify relevant wrapper
|
|
552
|
+
*/
|
|
553
|
+
checkAndNotifyWrapperState(event) {
|
|
554
|
+
// Stop events typically indicate Claude is waiting for input
|
|
555
|
+
if (event.hookType === 'Stop' || event.hookType === 'Notification') {
|
|
556
|
+
// Try managed wrappers first
|
|
557
|
+
for (const wrapper of this.wrapperManager.getAll()) {
|
|
558
|
+
if (wrapper.claudeSessionId === event.sessionId) {
|
|
559
|
+
this.wrapperManager.updateState(wrapper.wrapperId, 'waiting_input');
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
// Try to match by working directory
|
|
563
|
+
if (!wrapper.claudeSessionId && event.workingDirectory === wrapper.cwd) {
|
|
564
|
+
this.wrapperManager.updateState(wrapper.wrapperId, 'waiting_input', event.sessionId);
|
|
565
|
+
console.log(`[Daemon] Associated managed wrapper ${wrapper.wrapperId} with Claude session ${event.sessionId}`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Try legacy wrappers
|
|
570
|
+
for (const [wrapperId, session] of this.legacyWrapperSessions) {
|
|
571
|
+
// Match by Claude session ID if we have it
|
|
572
|
+
if (session.claudeSessionId === event.sessionId) {
|
|
573
|
+
session.state = 'waiting_input';
|
|
574
|
+
session.socket.write(JSON.stringify({
|
|
575
|
+
type: 'state_update',
|
|
576
|
+
state: 'waiting_input',
|
|
577
|
+
}) + '\n');
|
|
578
|
+
this.broadcastToClients({
|
|
579
|
+
type: 'wrapper_state',
|
|
580
|
+
wrapperId,
|
|
581
|
+
state: 'waiting_input',
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
// Try to match by working directory
|
|
586
|
+
if (!session.claudeSessionId && event.workingDirectory === session.cwd) {
|
|
587
|
+
// Associate this Claude session with the wrapper
|
|
588
|
+
session.claudeSessionId = event.sessionId;
|
|
589
|
+
session.state = 'waiting_input';
|
|
590
|
+
console.log(`[Daemon] Associated legacy wrapper ${wrapperId} with Claude session ${event.sessionId}`);
|
|
591
|
+
session.socket.write(JSON.stringify({
|
|
592
|
+
type: 'state_update',
|
|
593
|
+
state: 'waiting_input',
|
|
594
|
+
}) + '\n');
|
|
595
|
+
this.broadcastToClients({
|
|
596
|
+
type: 'wrapper_state',
|
|
597
|
+
wrapperId,
|
|
598
|
+
state: 'waiting_input',
|
|
599
|
+
claudeSessionId: event.sessionId,
|
|
600
|
+
});
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// =============================================================================
|
|
607
|
+
// Circuit Breaker Handlers
|
|
608
|
+
// =============================================================================
|
|
609
|
+
/**
|
|
610
|
+
* Handle get circuit health request
|
|
611
|
+
*/
|
|
612
|
+
handleGetCircuitHealth(socket) {
|
|
613
|
+
const health = this.circuitBreaker.getAllHealth();
|
|
614
|
+
socket.write(JSON.stringify({
|
|
615
|
+
type: 'circuit_health',
|
|
616
|
+
health,
|
|
617
|
+
}) + '\n');
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Handle get session health request
|
|
621
|
+
*/
|
|
622
|
+
handleGetSessionHealth(socket, message) {
|
|
623
|
+
const { sessionId } = message;
|
|
624
|
+
const health = this.circuitBreaker.getSessionHealth(sessionId);
|
|
625
|
+
if (health) {
|
|
626
|
+
socket.write(JSON.stringify({
|
|
627
|
+
type: 'session_health',
|
|
628
|
+
health,
|
|
629
|
+
}) + '\n');
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
socket.write(JSON.stringify({
|
|
633
|
+
type: 'error',
|
|
634
|
+
message: `No health data for session ${sessionId}`,
|
|
635
|
+
}) + '\n');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Handle reset circuit request
|
|
640
|
+
*/
|
|
641
|
+
handleResetCircuit(message) {
|
|
642
|
+
const { sessionId } = message;
|
|
643
|
+
this.circuitBreaker.resetCircuit(sessionId);
|
|
644
|
+
console.log(`[Daemon] Circuit reset for session ${sessionId}`);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Handle update circuit config request
|
|
648
|
+
*/
|
|
649
|
+
handleUpdateCircuitConfig(socket, message) {
|
|
650
|
+
const { config } = message;
|
|
651
|
+
this.circuitBreaker.updateConfig(config);
|
|
652
|
+
// Broadcast updated config to all clients
|
|
653
|
+
this.broadcastToClients({
|
|
654
|
+
type: 'circuit_config',
|
|
655
|
+
config: this.circuitBreaker.getConfig(),
|
|
656
|
+
});
|
|
657
|
+
socket.write(JSON.stringify({
|
|
658
|
+
type: 'circuit_config',
|
|
659
|
+
config: this.circuitBreaker.getConfig(),
|
|
660
|
+
}) + '\n');
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Get the circuit breaker instance (for external access)
|
|
664
|
+
*/
|
|
665
|
+
getCircuitBreaker() {
|
|
666
|
+
return this.circuitBreaker;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Get list of active wrapper sessions (for UI)
|
|
670
|
+
*/
|
|
671
|
+
getWrapperSessions() {
|
|
672
|
+
const managed = this.wrapperManager.getAll().map(s => ({
|
|
673
|
+
wrapperId: s.wrapperId,
|
|
674
|
+
state: s.state,
|
|
675
|
+
claudeSessionId: s.claudeSessionId,
|
|
676
|
+
managed: true,
|
|
677
|
+
}));
|
|
678
|
+
const legacy = Array.from(this.legacyWrapperSessions.values()).map(s => ({
|
|
679
|
+
wrapperId: s.wrapperId,
|
|
680
|
+
state: s.state,
|
|
681
|
+
claudeSessionId: s.claudeSessionId,
|
|
682
|
+
managed: false,
|
|
683
|
+
}));
|
|
684
|
+
return [...managed, ...legacy];
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
exports.MonitorDaemon = MonitorDaemon;
|
|
688
|
+
/**
|
|
689
|
+
* Start the daemon (exported for CLI)
|
|
690
|
+
*/
|
|
691
|
+
async function startDaemon(config) {
|
|
692
|
+
const daemon = new MonitorDaemon(config);
|
|
693
|
+
await daemon.start();
|
|
694
|
+
return daemon;
|
|
695
|
+
}
|