coder-config 0.50.5-beta → 0.50.7-beta
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/config-loader.js +49 -0
- package/lib/cli.js +13 -0
- package/lib/constants.js +1 -1
- package/lib/heartbeat.js +141 -1
- package/package.json +1 -1
package/config-loader.js
CHANGED
|
@@ -31,6 +31,7 @@ const { getProjectsRegistryPath, loadProjectsRegistry, saveProjectsRegistry, pro
|
|
|
31
31
|
const { getWorkstreamsPath, loadWorkstreams, saveWorkstreams, workstreamList, workstreamCreate, workstreamUpdate, workstreamDelete, workstreamUse, workstreamActive, workstreamAddProject, workstreamRemoveProject, workstreamInject, workstreamDetect, workstreamGet, getActiveWorkstream, countWorkstreamsForProject, workstreamInstallHook, workstreamInstallHookGemini, workstreamInstallHookCodex, workstreamDeactivate, workstreamCheckPath, getSettingsPath, loadSettings, saveSettings, workstreamAddTrigger, workstreamRemoveTrigger, workstreamSetAutoActivate, setGlobalAutoActivate, shouldAutoActivate, workstreamCheckFolder, workstreamInstallCdHook, workstreamUninstallCdHook, workstreamCdHookStatus, discoverSubProjects, generateRulesFromRepos, generateRulesWithClaude, generateRulesWithAI, getAvailableAITools, findAIBinary, AI_TOOLS, workstreamSetSandbox } = require('./lib/workstreams');
|
|
32
32
|
const { getActivityPath, getDefaultActivity, loadActivity, saveActivity, detectProjectRoot, activityLog, activitySummary, generateWorkstreamName, activitySuggestWorkstreams, activityClear } = require('./lib/activity');
|
|
33
33
|
const { getLoopsPath, loadLoops, saveLoops, loadLoopState, saveLoopState, loadHistory, saveHistory, loopList, loopCreate, loopGet, loopUpdate, loopDelete, loopStart, loopPause, loopResume, loopCancel, loopApprove, loopComplete, loopFail, loopStatus, loopHistory, loopConfig, getActiveLoop, recordIteration, saveClarifications, savePlan, loadClarifications, loadPlan, loopInject, archiveLoop } = require('./lib/loops');
|
|
34
|
+
const { heartbeat: runHeartbeat, formatReport, getExitCode, saveLastHeartbeat, shouldNotify, buildMacosNotification, loadHeartbeatConfig, getDefaultHeartbeatConfig } = require('./lib/heartbeat');
|
|
34
35
|
const { getSessionStatus, showSessionStatus, flushContext, clearContext, installHooks: sessionInstallHooks, getFlushedContext, installFlushCommand, installAll: sessionInstallAll, SESSION_DIR, FLUSHED_CONTEXT_FILE } = require('./lib/sessions');
|
|
35
36
|
const { runCli } = require('./lib/cli');
|
|
36
37
|
const { shellStatus, shellInstall, shellUninstall, printShellStatus } = require('./lib/shell');
|
|
@@ -253,6 +254,54 @@ class ClaudeConfigManager {
|
|
|
253
254
|
loopInject(silent) { return loopInject(this.installDir, silent); }
|
|
254
255
|
archiveLoop(loopId) { return archiveLoop(this.installDir, loopId); }
|
|
255
256
|
|
|
257
|
+
loopHeartbeat(options = {}) {
|
|
258
|
+
const report = runHeartbeat(this.installDir);
|
|
259
|
+
|
|
260
|
+
if (options.json) {
|
|
261
|
+
console.log(JSON.stringify(report, null, 2));
|
|
262
|
+
} else if (options.quiet) {
|
|
263
|
+
if (getExitCode(report) === 1) {
|
|
264
|
+
console.log(formatReport(report));
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
console.log(formatReport(report));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (options.notify) {
|
|
271
|
+
const config = loadHeartbeatConfig(this.installDir);
|
|
272
|
+
const newAlerts = report.alerts.filter(a =>
|
|
273
|
+
(a.severity === 'critical' || a.severity === 'warning') &&
|
|
274
|
+
shouldNotify(this.installDir, a, config.cooldownMinutes || 15)
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (newAlerts.length > 0) {
|
|
278
|
+
if (config.notifications?.macos?.enabled !== false) {
|
|
279
|
+
const cmd = buildMacosNotification(report);
|
|
280
|
+
try {
|
|
281
|
+
require('child_process').execSync(cmd);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
// osascript may fail in non-GUI contexts
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (config.notifications?.slack?.enabled && config.notifications?.slack?.webhookUrl) {
|
|
288
|
+
const https = require('https');
|
|
289
|
+
const url = new URL(config.notifications.slack.webhookUrl);
|
|
290
|
+
const payload = JSON.stringify({ text: `Ralph Heartbeat: ${report.summary}` });
|
|
291
|
+
const req = https.request({ hostname: url.hostname, path: url.pathname, method: 'POST',
|
|
292
|
+
headers: { 'Content-Type': 'application/json' }
|
|
293
|
+
});
|
|
294
|
+
req.write(payload);
|
|
295
|
+
req.end();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
saveLastHeartbeat(this.installDir, report);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return report;
|
|
303
|
+
}
|
|
304
|
+
|
|
256
305
|
// Activity
|
|
257
306
|
getActivityPath() { return getActivityPath(this.installDir); }
|
|
258
307
|
loadActivity() { return loadActivity(this.installDir); }
|
package/lib/cli.js
CHANGED
|
@@ -272,6 +272,16 @@ function runCli(manager) {
|
|
|
272
272
|
} else {
|
|
273
273
|
manager.loopConfig();
|
|
274
274
|
}
|
|
275
|
+
} else if (args[1] === 'heartbeat') {
|
|
276
|
+
const options = {
|
|
277
|
+
notify: args.includes('--notify'),
|
|
278
|
+
quiet: args.includes('--quiet'),
|
|
279
|
+
json: args.includes('--json')
|
|
280
|
+
};
|
|
281
|
+
const report = manager.loopHeartbeat(options);
|
|
282
|
+
if (options.quiet) {
|
|
283
|
+
process.exitCode = report.alerts.some(a => a.severity === 'critical' || a.severity === 'warning') ? 1 : 0;
|
|
284
|
+
}
|
|
275
285
|
} else if (args[1] === 'inject') {
|
|
276
286
|
const silent = args.includes('--silent') || args.includes('-s');
|
|
277
287
|
manager.loopInject(silent);
|
|
@@ -456,6 +466,9 @@ ${chalk.dim('Configuration manager for AI coding tools (Claude Code, Gemini CLI,
|
|
|
456
466
|
cmd('loop cancel <id>', 'Cancel loop'),
|
|
457
467
|
cmd('loop status [id]', 'Show loop status'),
|
|
458
468
|
cmd('loop config', 'Show/set loop config'),
|
|
469
|
+
cmd('loop heartbeat', 'Check loop health'),
|
|
470
|
+
cmd('loop heartbeat --notify', 'Check + send notifications'),
|
|
471
|
+
cmd('loop heartbeat --quiet', 'Silent unless alerts'),
|
|
459
472
|
]));
|
|
460
473
|
console.log();
|
|
461
474
|
}
|
package/lib/constants.js
CHANGED
package/lib/heartbeat.js
CHANGED
|
@@ -199,10 +199,150 @@ function heartbeat(installDir, config) {
|
|
|
199
199
|
};
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Save the last heartbeat report to disk for deduplication
|
|
204
|
+
* @param {string} installDir - path to coder-config install dir
|
|
205
|
+
* @param {object} report - heartbeat report with alerts array
|
|
206
|
+
*/
|
|
207
|
+
function saveLastHeartbeat(installDir, report) {
|
|
208
|
+
const loopsDir = path.join(installDir, 'loops');
|
|
209
|
+
fs.mkdirSync(loopsDir, { recursive: true });
|
|
210
|
+
const alertHashes = {};
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
for (const alert of (report.alerts || [])) {
|
|
213
|
+
const key = `${alert.loopId}:${alert.type}`;
|
|
214
|
+
alertHashes[key] = now;
|
|
215
|
+
}
|
|
216
|
+
const data = { alertHashes, timestamp: now };
|
|
217
|
+
fs.writeFileSync(path.join(loopsDir, 'last-heartbeat.json'), JSON.stringify(data, null, 2));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Load the last saved heartbeat from disk
|
|
222
|
+
* @param {string} installDir - path to coder-config install dir
|
|
223
|
+
* @returns {object|null} saved heartbeat data or null if not found
|
|
224
|
+
*/
|
|
225
|
+
function loadLastHeartbeat(installDir) {
|
|
226
|
+
const filePath = path.join(installDir, 'loops', 'last-heartbeat.json');
|
|
227
|
+
try {
|
|
228
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
229
|
+
return JSON.parse(raw);
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Determine whether an alert should fire (not within cooldown)
|
|
237
|
+
* @param {string} installDir - path to coder-config install dir
|
|
238
|
+
* @param {object} alert - alert object with loopId and type
|
|
239
|
+
* @param {number} cooldownMinutes - cooldown in minutes
|
|
240
|
+
* @returns {boolean} true if should notify, false if within cooldown
|
|
241
|
+
*/
|
|
242
|
+
function shouldNotify(installDir, alert, cooldownMinutes) {
|
|
243
|
+
const last = loadLastHeartbeat(installDir);
|
|
244
|
+
if (!last || !last.alertHashes) return true;
|
|
245
|
+
const key = `${alert.loopId}:${alert.type}`;
|
|
246
|
+
const lastTime = last.alertHashes[key];
|
|
247
|
+
if (lastTime == null) return true;
|
|
248
|
+
const ageMinutes = (Date.now() - lastTime) / 60000;
|
|
249
|
+
return ageMinutes >= cooldownMinutes;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build a macOS osascript notification command for a heartbeat report
|
|
254
|
+
* @param {object} report - heartbeat report
|
|
255
|
+
* @returns {string} shell command string
|
|
256
|
+
*/
|
|
257
|
+
function buildMacosNotification(report) {
|
|
258
|
+
const alerts = report.alerts || [];
|
|
259
|
+
const criticalCount = alerts.filter(a => a.severity === 'critical').length;
|
|
260
|
+
const warningCount = alerts.filter(a => a.severity === 'warning').length;
|
|
261
|
+
const parts = [];
|
|
262
|
+
if (criticalCount > 0) parts.push(`${criticalCount} critical`);
|
|
263
|
+
if (warningCount > 0) parts.push(`${warningCount} warning${warningCount !== 1 ? 's' : ''}`);
|
|
264
|
+
const subtitle = parts.length > 0 ? parts.join(', ') : 'all healthy';
|
|
265
|
+
const body = (report.summary || subtitle).replace(/'/g, "\\'");
|
|
266
|
+
return `osascript -e 'display notification "${body}" with title "Ralph Heartbeat" subtitle "${subtitle}"'`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get exit code for a heartbeat report
|
|
271
|
+
* @param {object} report - heartbeat report with alerts array
|
|
272
|
+
* @returns {number} 0 if healthy or only info, 1 if any warning or critical
|
|
273
|
+
*/
|
|
274
|
+
function getExitCode(report) {
|
|
275
|
+
const alerts = report.alerts || [];
|
|
276
|
+
const hasActionable = alerts.some(a => a.severity === 'critical' || a.severity === 'warning');
|
|
277
|
+
return hasActionable ? 1 : 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Format a heartbeat report for terminal display
|
|
282
|
+
* @param {object} report - heartbeat report object
|
|
283
|
+
* @returns {string} formatted string for console output
|
|
284
|
+
*/
|
|
285
|
+
function formatReport(report) {
|
|
286
|
+
const chalk = require('chalk');
|
|
287
|
+
const lines = [];
|
|
288
|
+
|
|
289
|
+
lines.push(`♥ Loop Heartbeat — ${report.timestamp}`);
|
|
290
|
+
lines.push('');
|
|
291
|
+
|
|
292
|
+
const alerts = report.alerts || [];
|
|
293
|
+
const critical = alerts.filter(a => a.severity === 'critical');
|
|
294
|
+
const warning = alerts.filter(a => a.severity === 'warning');
|
|
295
|
+
const info = alerts.filter(a => a.severity === 'info');
|
|
296
|
+
const healthy = report.healthy || [];
|
|
297
|
+
|
|
298
|
+
if (critical.length > 0) {
|
|
299
|
+
lines.push(chalk.red('🔴 CRITICAL'));
|
|
300
|
+
for (const a of critical) {
|
|
301
|
+
lines.push(` ${chalk.red('✗')} ${a.name} — ${a.message}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push('');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (warning.length > 0) {
|
|
307
|
+
lines.push(chalk.yellow('🟡 WARNING'));
|
|
308
|
+
for (const a of warning) {
|
|
309
|
+
lines.push(` ${chalk.yellow('⚠')} ${a.name} — ${a.message}`);
|
|
310
|
+
}
|
|
311
|
+
lines.push('');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (info.length > 0) {
|
|
315
|
+
lines.push(chalk.blue('🔵 INFO'));
|
|
316
|
+
for (const a of info) {
|
|
317
|
+
lines.push(` ${chalk.blue('ℹ')} ${a.name} — ${a.message}`);
|
|
318
|
+
}
|
|
319
|
+
lines.push('');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (healthy.length > 0) {
|
|
323
|
+
lines.push(chalk.green('🟢 HEALTHY'));
|
|
324
|
+
for (const h of healthy) {
|
|
325
|
+
const phase = h.phase ? ` [${h.phase}]` : '';
|
|
326
|
+
lines.push(` ${chalk.green('●')} ${h.name}${phase} ${h.iteration}`);
|
|
327
|
+
}
|
|
328
|
+
lines.push('');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
lines.push(`Summary: ${report.summary}`);
|
|
332
|
+
|
|
333
|
+
return lines.join('\n');
|
|
334
|
+
}
|
|
335
|
+
|
|
202
336
|
module.exports = {
|
|
203
337
|
heartbeat,
|
|
204
338
|
getDefaultHeartbeatConfig,
|
|
205
339
|
loadHeartbeatConfig,
|
|
206
340
|
evaluateLoop,
|
|
207
|
-
buildSummary
|
|
341
|
+
buildSummary,
|
|
342
|
+
saveLastHeartbeat,
|
|
343
|
+
loadLastHeartbeat,
|
|
344
|
+
shouldNotify,
|
|
345
|
+
buildMacosNotification,
|
|
346
|
+
getExitCode,
|
|
347
|
+
formatReport
|
|
208
348
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coder-config",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.7-beta",
|
|
4
4
|
"description": "Configuration manager for AI coding tools - Claude Code, Gemini CLI, Codex CLI, Antigravity. Manage MCPs, rules, permissions, memory, and workstreams.",
|
|
5
5
|
"author": "regression.io",
|
|
6
6
|
"main": "config-loader.js",
|