@cluesmith/codev 1.6.0 → 1.6.2

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,366 @@
1
+ /**
2
+ * HQ Connector - Connects Agent Farm to CODEV_HQ
3
+ *
4
+ * When CODEV_HQ_URL is set, this module:
5
+ * 1. Establishes WebSocket connection to HQ
6
+ * 2. Registers with project info
7
+ * 3. Watches status files and syncs changes
8
+ * 4. Handles approval messages from HQ
9
+ */
10
+ import { WebSocket } from 'ws';
11
+ import { watch, existsSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { join, basename, dirname, relative } from 'node:path';
13
+ import { randomUUID } from 'node:crypto';
14
+ import { execSync } from 'node:child_process';
15
+ import { logger } from './utils/logger.js';
16
+ /**
17
+ * HQ Connector state
18
+ */
19
+ let ws = null;
20
+ let reconnectTimer = null;
21
+ let pingInterval = null;
22
+ let statusWatcher = null;
23
+ let instanceId = null;
24
+ let projectRoot = null;
25
+ let reconnectAttempts = 0;
26
+ const MAX_RECONNECT_DELAY = 60000;
27
+ const BASE_RECONNECT_DELAY = 1000;
28
+ /**
29
+ * Generate a unique message ID
30
+ */
31
+ function generateId() {
32
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
33
+ }
34
+ /**
35
+ * Send a message to HQ
36
+ */
37
+ function sendMessage(message) {
38
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
39
+ logger.debug('[HQ] Cannot send - WebSocket not open');
40
+ return;
41
+ }
42
+ ws.send(JSON.stringify(message));
43
+ }
44
+ /**
45
+ * Get git remote URL for a project
46
+ */
47
+ function getGitRemote(projectPath) {
48
+ try {
49
+ const remote = execSync('git remote get-url origin', {
50
+ cwd: projectPath,
51
+ encoding: 'utf-8',
52
+ timeout: 5000,
53
+ }).trim();
54
+ return remote || undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ /**
61
+ * Get current git SHA for a file
62
+ */
63
+ function getGitSha(filePath) {
64
+ try {
65
+ const sha = execSync(`git log -1 --format=%H -- "${filePath}"`, {
66
+ cwd: dirname(filePath),
67
+ encoding: 'utf-8',
68
+ timeout: 5000,
69
+ }).trim();
70
+ return sha || undefined;
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ /**
77
+ * Create git commit for approval
78
+ */
79
+ function commitApproval(filePath, gate, approvedBy) {
80
+ try {
81
+ const dir = dirname(filePath);
82
+ const fileName = basename(filePath);
83
+ execSync(`git add "${fileName}"`, { cwd: dir, timeout: 5000 });
84
+ execSync(`git commit -m "[HQ] Gate approved: ${gate} by ${approvedBy}"`, {
85
+ cwd: dir,
86
+ timeout: 5000,
87
+ });
88
+ return true;
89
+ }
90
+ catch (error) {
91
+ logger.debug(`[HQ] Failed to commit approval: ${error instanceof Error ? error.message : error}`);
92
+ return false;
93
+ }
94
+ }
95
+ /**
96
+ * Handle incoming messages from HQ
97
+ */
98
+ function handleMessage(data) {
99
+ let message;
100
+ try {
101
+ message = JSON.parse(data);
102
+ }
103
+ catch {
104
+ logger.debug('[HQ] Invalid message received');
105
+ return;
106
+ }
107
+ switch (message.type) {
108
+ case 'welcome':
109
+ logger.info('[HQ] Connected to CODEV_HQ');
110
+ register();
111
+ break;
112
+ case 'pong':
113
+ logger.debug('[HQ] Pong received');
114
+ break;
115
+ case 'response':
116
+ const payload = message.payload;
117
+ if (payload?.success === false) {
118
+ logger.warn(`[HQ] Error response: ${payload.error}`);
119
+ }
120
+ break;
121
+ case 'approval':
122
+ handleApproval(message);
123
+ break;
124
+ case 'command':
125
+ handleCommand(message);
126
+ break;
127
+ default:
128
+ logger.debug(`[HQ] Unknown message type: ${message.type}`);
129
+ }
130
+ }
131
+ /**
132
+ * Handle approval message from HQ
133
+ */
134
+ function handleApproval(message) {
135
+ const payload = message.payload;
136
+ logger.info(`[HQ] Received approval for ${payload.gate} from ${payload.approved_by}`);
137
+ // Find the status file for this project
138
+ const statusDir = join(projectRoot, 'codev', 'status');
139
+ const statusFile = join(statusDir, `${payload.project_id}-*.md`);
140
+ // For spike simplicity, just look for any file starting with project_id
141
+ try {
142
+ const { globSync } = require('glob');
143
+ const files = globSync(statusFile);
144
+ if (files.length === 0) {
145
+ logger.warn(`[HQ] Status file not found for project ${payload.project_id}`);
146
+ return;
147
+ }
148
+ const filePath = files[0];
149
+ let content = readFileSync(filePath, 'utf-8');
150
+ // Parse YAML frontmatter and update gate status
151
+ // Simple regex-based update for spike
152
+ const gatePattern = new RegExp(`(${payload.gate.replace(/\./g, '\\.')}):\\s*\\{\\s*status:\\s*pending`, 'g');
153
+ const newContent = content.replace(gatePattern, `$1: { status: passed, by: ${payload.approved_by}, at: ${payload.approved_at}`);
154
+ if (newContent !== content) {
155
+ writeFileSync(filePath, newContent, 'utf-8');
156
+ logger.info(`[HQ] Updated ${basename(filePath)} with approval`);
157
+ // Create git commit
158
+ if (commitApproval(filePath, payload.gate, payload.approved_by)) {
159
+ logger.info(`[HQ] Created git commit for approval`);
160
+ }
161
+ // Send status update back to HQ
162
+ sendStatusUpdate(payload.project_path, filePath, newContent);
163
+ }
164
+ }
165
+ catch (error) {
166
+ logger.error(`[HQ] Failed to handle approval: ${error instanceof Error ? error.message : error}`);
167
+ }
168
+ }
169
+ /**
170
+ * Handle command from HQ
171
+ */
172
+ function handleCommand(message) {
173
+ const payload = message.payload;
174
+ logger.info(`[HQ] Received command: ${payload.command}`);
175
+ // For spike, just log commands - full implementation would execute them
176
+ logger.debug(`[HQ] Command args: ${JSON.stringify(payload.args)}`);
177
+ // Acknowledge receipt
178
+ sendMessage({
179
+ type: 'command_ack',
180
+ id: generateId(),
181
+ ts: Date.now(),
182
+ payload: { command_id: message.id, status: 'received' },
183
+ });
184
+ }
185
+ /**
186
+ * Register with HQ
187
+ */
188
+ function register() {
189
+ if (!projectRoot)
190
+ return;
191
+ const projects = [{
192
+ path: projectRoot,
193
+ name: basename(projectRoot),
194
+ git_remote: getGitRemote(projectRoot),
195
+ }];
196
+ sendMessage({
197
+ type: 'register',
198
+ id: generateId(),
199
+ ts: Date.now(),
200
+ payload: {
201
+ instance_id: instanceId,
202
+ instance_name: process.env.CODEV_HQ_INSTANCE_NAME || `${require('os').hostname()}-agent-farm`,
203
+ version: require('../../package.json').version,
204
+ projects,
205
+ },
206
+ });
207
+ // Start status file watching after registration
208
+ startStatusWatcher();
209
+ // Reset reconnect counter on successful registration
210
+ reconnectAttempts = 0;
211
+ }
212
+ /**
213
+ * Send status file update to HQ
214
+ */
215
+ function sendStatusUpdate(projectPath, statusFile, content) {
216
+ sendMessage({
217
+ type: 'status_update',
218
+ id: generateId(),
219
+ ts: Date.now(),
220
+ payload: {
221
+ project_path: projectPath,
222
+ status_file: relative(projectPath, statusFile),
223
+ content,
224
+ git_sha: getGitSha(statusFile),
225
+ },
226
+ });
227
+ }
228
+ /**
229
+ * Watch status files for changes
230
+ */
231
+ function startStatusWatcher() {
232
+ if (statusWatcher)
233
+ return;
234
+ if (!projectRoot)
235
+ return;
236
+ const statusDir = join(projectRoot, 'codev', 'status');
237
+ // Create status directory if it doesn't exist
238
+ if (!existsSync(statusDir)) {
239
+ logger.debug('[HQ] Status directory does not exist, skipping watcher');
240
+ return;
241
+ }
242
+ logger.info('[HQ] Watching status files...');
243
+ statusWatcher = watch(statusDir, (eventType, filename) => {
244
+ if (!filename || !filename.endsWith('.md'))
245
+ return;
246
+ const filePath = join(statusDir, filename);
247
+ if (!existsSync(filePath))
248
+ return;
249
+ try {
250
+ const content = readFileSync(filePath, 'utf-8');
251
+ sendStatusUpdate(projectRoot, filePath, content);
252
+ logger.debug(`[HQ] Synced ${filename}`);
253
+ }
254
+ catch (error) {
255
+ logger.debug(`[HQ] Failed to read ${filename}: ${error instanceof Error ? error.message : error}`);
256
+ }
257
+ });
258
+ }
259
+ /**
260
+ * Start ping interval to keep connection alive
261
+ */
262
+ function startPingInterval() {
263
+ if (pingInterval)
264
+ return;
265
+ pingInterval = setInterval(() => {
266
+ sendMessage({
267
+ type: 'ping',
268
+ id: generateId(),
269
+ ts: Date.now(),
270
+ payload: { ts: Date.now() },
271
+ });
272
+ }, 30000);
273
+ }
274
+ /**
275
+ * Attempt to reconnect to HQ
276
+ */
277
+ function scheduleReconnect() {
278
+ if (reconnectTimer)
279
+ return;
280
+ const delay = Math.min(BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts), MAX_RECONNECT_DELAY);
281
+ reconnectAttempts++;
282
+ logger.info(`[HQ] Reconnecting in ${delay / 1000}s...`);
283
+ reconnectTimer = setTimeout(() => {
284
+ reconnectTimer = null;
285
+ connect();
286
+ }, delay);
287
+ }
288
+ /**
289
+ * Connect to CODEV_HQ
290
+ */
291
+ function connect() {
292
+ const hqUrl = process.env.CODEV_HQ_URL;
293
+ if (!hqUrl)
294
+ return;
295
+ logger.info(`[HQ] Connecting to ${hqUrl}...`);
296
+ const apiKey = process.env.CODEV_HQ_API_KEY || 'dev-key-spike';
297
+ const wsUrl = new URL(hqUrl);
298
+ wsUrl.searchParams.set('key', apiKey);
299
+ ws = new WebSocket(wsUrl.toString());
300
+ ws.on('open', () => {
301
+ logger.info('[HQ] WebSocket connected');
302
+ startPingInterval();
303
+ });
304
+ ws.on('message', (data) => {
305
+ handleMessage(data.toString());
306
+ });
307
+ ws.on('close', (code, reason) => {
308
+ logger.warn(`[HQ] Disconnected (code: ${code})`);
309
+ cleanup();
310
+ scheduleReconnect();
311
+ });
312
+ ws.on('error', (error) => {
313
+ logger.error(`[HQ] WebSocket error: ${error.message}`);
314
+ });
315
+ }
316
+ /**
317
+ * Cleanup resources
318
+ */
319
+ function cleanup() {
320
+ if (pingInterval) {
321
+ clearInterval(pingInterval);
322
+ pingInterval = null;
323
+ }
324
+ if (statusWatcher) {
325
+ statusWatcher.close();
326
+ statusWatcher = null;
327
+ }
328
+ ws = null;
329
+ }
330
+ /**
331
+ * Initialize HQ connector
332
+ * Called from af start when CODEV_HQ_URL is set
333
+ */
334
+ export function initHQConnector(root) {
335
+ const hqUrl = process.env.CODEV_HQ_URL;
336
+ if (!hqUrl) {
337
+ logger.debug('[HQ] CODEV_HQ_URL not set, HQ connector disabled');
338
+ return;
339
+ }
340
+ instanceId = randomUUID();
341
+ projectRoot = root;
342
+ logger.info('[HQ] HQ connector enabled');
343
+ connect();
344
+ }
345
+ /**
346
+ * Stop HQ connector
347
+ */
348
+ export function stopHQConnector() {
349
+ if (reconnectTimer) {
350
+ clearTimeout(reconnectTimer);
351
+ reconnectTimer = null;
352
+ }
353
+ cleanup();
354
+ if (ws) {
355
+ ws.close();
356
+ ws = null;
357
+ }
358
+ logger.info('[HQ] Disconnected from CODEV_HQ');
359
+ }
360
+ /**
361
+ * Check if HQ connector is enabled
362
+ */
363
+ export function isHQEnabled() {
364
+ return !!process.env.CODEV_HQ_URL;
365
+ }
366
+ //# sourceMappingURL=hq-connector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hq-connector.js","sourceRoot":"","sources":["../../src/agent-farm/hq-connector.ts"],"names":[],"mappings":"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"}
@@ -1552,6 +1552,52 @@ const server = http.createServer(async (req, res) => {
1552
1552
  res.end(JSON.stringify(tree));
1553
1553
  return;
1554
1554
  }
1555
+ // API: Get hash of file tree for change detection (auto-refresh)
1556
+ if (req.method === 'GET' && url.pathname === '/api/files/hash') {
1557
+ // Build a lightweight hash based on directory mtimes
1558
+ // This is faster than building the full tree
1559
+ function getTreeHash(dirPath) {
1560
+ const EXCLUDED_DIRS = new Set([
1561
+ 'node_modules', '.git', 'dist', '__pycache__', '.next',
1562
+ '.nuxt', '.turbo', 'coverage', '.nyc_output', '.cache',
1563
+ '.parcel-cache', 'build', '.svelte-kit', 'vendor', '.venv', 'venv', 'env',
1564
+ ]);
1565
+ let hash = '';
1566
+ function walk(dir) {
1567
+ try {
1568
+ const stat = fs.statSync(dir);
1569
+ hash += `${dir}:${stat.mtimeMs};`;
1570
+ const items = fs.readdirSync(dir, { withFileTypes: true });
1571
+ for (const item of items) {
1572
+ if (EXCLUDED_DIRS.has(item.name))
1573
+ continue;
1574
+ if (item.isDirectory()) {
1575
+ walk(path.join(dir, item.name));
1576
+ }
1577
+ else if (item.isFile()) {
1578
+ // Include file mtime for change detection
1579
+ const fileStat = fs.statSync(path.join(dir, item.name));
1580
+ hash += `${item.name}:${fileStat.mtimeMs};`;
1581
+ }
1582
+ }
1583
+ }
1584
+ catch {
1585
+ // Ignore errors
1586
+ }
1587
+ }
1588
+ walk(dirPath);
1589
+ // Simple hash: sum of char codes
1590
+ let sum = 0;
1591
+ for (let i = 0; i < hash.length; i++) {
1592
+ sum = ((sum << 5) - sum + hash.charCodeAt(i)) | 0;
1593
+ }
1594
+ return sum.toString(16);
1595
+ }
1596
+ const hash = getTreeHash(projectRoot);
1597
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1598
+ res.end(JSON.stringify({ hash }));
1599
+ return;
1600
+ }
1555
1601
  // API: Create a new file (Bugfix #131)
1556
1602
  if (req.method === 'POST' && url.pathname === '/api/files') {
1557
1603
  const body = await parseJsonBody(req);