coder-config 0.50.6-beta → 0.50.8-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 +23 -4
- package/lib/constants.js +1 -1
- package/lib/heartbeat.js +58 -1
- package/lib/loops.js +63 -1
- package/package.json +1 -1
- package/ui/routes/loops.js +7 -0
- package/ui/server.cjs +4 -0
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
|
@@ -263,15 +263,31 @@ function runCli(manager) {
|
|
|
263
263
|
manager.loopHistory();
|
|
264
264
|
} else if (args[1] === 'config') {
|
|
265
265
|
const updates = {};
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
// Dot-notation key=value: coder-config loop config heartbeat.staleThresholdMinutes 30
|
|
267
|
+
if (args[2] && args[3] && !args[2].startsWith('--')) {
|
|
268
|
+
updates[args[2]] = args[3];
|
|
269
|
+
} else {
|
|
270
|
+
// Flag-based (backwards compatible)
|
|
271
|
+
const maxIterIdx = args.indexOf('--max-iterations');
|
|
272
|
+
if (maxIterIdx !== -1) updates.maxIterations = args[maxIterIdx + 1];
|
|
273
|
+
if (args.includes('--auto-approve-plan')) updates.autoApprovePlan = true;
|
|
274
|
+
if (args.includes('--no-auto-approve-plan')) updates.autoApprovePlan = false;
|
|
275
|
+
}
|
|
270
276
|
if (Object.keys(updates).length > 0) {
|
|
271
277
|
manager.loopConfig(updates);
|
|
272
278
|
} else {
|
|
273
279
|
manager.loopConfig();
|
|
274
280
|
}
|
|
281
|
+
} else if (args[1] === 'heartbeat') {
|
|
282
|
+
const options = {
|
|
283
|
+
notify: args.includes('--notify'),
|
|
284
|
+
quiet: args.includes('--quiet'),
|
|
285
|
+
json: args.includes('--json')
|
|
286
|
+
};
|
|
287
|
+
const report = manager.loopHeartbeat(options);
|
|
288
|
+
if (options.quiet) {
|
|
289
|
+
process.exitCode = report.alerts.some(a => a.severity === 'critical' || a.severity === 'warning') ? 1 : 0;
|
|
290
|
+
}
|
|
275
291
|
} else if (args[1] === 'inject') {
|
|
276
292
|
const silent = args.includes('--silent') || args.includes('-s');
|
|
277
293
|
manager.loopInject(silent);
|
|
@@ -456,6 +472,9 @@ ${chalk.dim('Configuration manager for AI coding tools (Claude Code, Gemini CLI,
|
|
|
456
472
|
cmd('loop cancel <id>', 'Cancel loop'),
|
|
457
473
|
cmd('loop status [id]', 'Show loop status'),
|
|
458
474
|
cmd('loop config', 'Show/set loop config'),
|
|
475
|
+
cmd('loop heartbeat', 'Check loop health'),
|
|
476
|
+
cmd('loop heartbeat --notify', 'Check + send notifications'),
|
|
477
|
+
cmd('loop heartbeat --quiet', 'Silent unless alerts'),
|
|
459
478
|
]));
|
|
460
479
|
console.log();
|
|
461
480
|
}
|
package/lib/constants.js
CHANGED
package/lib/heartbeat.js
CHANGED
|
@@ -277,6 +277,62 @@ function getExitCode(report) {
|
|
|
277
277
|
return hasActionable ? 1 : 0;
|
|
278
278
|
}
|
|
279
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
|
+
|
|
280
336
|
module.exports = {
|
|
281
337
|
heartbeat,
|
|
282
338
|
getDefaultHeartbeatConfig,
|
|
@@ -287,5 +343,6 @@ module.exports = {
|
|
|
287
343
|
loadLastHeartbeat,
|
|
288
344
|
shouldNotify,
|
|
289
345
|
buildMacosNotification,
|
|
290
|
-
getExitCode
|
|
346
|
+
getExitCode,
|
|
347
|
+
formatReport
|
|
291
348
|
};
|
package/lib/loops.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
|
+
const { getDefaultHeartbeatConfig } = require('./heartbeat');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Get loops directory path
|
|
@@ -621,6 +622,32 @@ function archiveLoop(installDir, loopId) {
|
|
|
621
622
|
}
|
|
622
623
|
}
|
|
623
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Set a nested value on an object using dot-notation path.
|
|
627
|
+
* Creates intermediate objects as needed.
|
|
628
|
+
*/
|
|
629
|
+
function setNestedValue(obj, dotPath, value) {
|
|
630
|
+
const parts = dotPath.split('.');
|
|
631
|
+
let current = obj;
|
|
632
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
633
|
+
if (current[parts[i]] === undefined || typeof current[parts[i]] !== 'object') {
|
|
634
|
+
current[parts[i]] = {};
|
|
635
|
+
}
|
|
636
|
+
current = current[parts[i]];
|
|
637
|
+
}
|
|
638
|
+
current[parts[parts.length - 1]] = value;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Auto-convert string values to appropriate types.
|
|
643
|
+
*/
|
|
644
|
+
function coerceValue(value) {
|
|
645
|
+
if (value === 'true') return true;
|
|
646
|
+
if (value === 'false') return false;
|
|
647
|
+
if (typeof value === 'string' && /^\d+$/.test(value)) return parseInt(value, 10);
|
|
648
|
+
return value;
|
|
649
|
+
}
|
|
650
|
+
|
|
624
651
|
/**
|
|
625
652
|
* Get/set loop configuration
|
|
626
653
|
*/
|
|
@@ -634,11 +661,46 @@ function loopConfig(installDir, updates = null) {
|
|
|
634
661
|
console.log(` Auto-approve Plan: ${data.config.autoApprovePlan}`);
|
|
635
662
|
console.log(` Max Clarify Iterations: ${data.config.maxClarifyIterations}`);
|
|
636
663
|
console.log(` Completion Promise: ${data.config.completionPromise || 'DONE'}`);
|
|
664
|
+
if (data.config.heartbeat) {
|
|
665
|
+
const hb = data.config.heartbeat;
|
|
666
|
+
const slack = hb.notifications && hb.notifications.slack;
|
|
667
|
+
const macos = hb.notifications && hb.notifications.macos;
|
|
668
|
+
console.log(' Heartbeat:');
|
|
669
|
+
console.log(` Stale Threshold: ${hb.staleThresholdMinutes !== undefined ? hb.staleThresholdMinutes + 'm' : '30m'}`);
|
|
670
|
+
console.log(` Iteration Limit: ${hb.iterationLimitPercent !== undefined ? hb.iterationLimitPercent + '%' : '80%'}`);
|
|
671
|
+
console.log(` Cooldown: ${hb.cooldownMinutes !== undefined ? hb.cooldownMinutes + 'm' : '15m'}`);
|
|
672
|
+
console.log(` macOS Notifications: ${macos && macos.enabled !== undefined ? (macos.enabled ? 'on' : 'off') : 'on'}`);
|
|
673
|
+
if (slack) {
|
|
674
|
+
const slackStatus = slack.enabled ? `on (${slack.channel || '#dev-loops'})` : 'off';
|
|
675
|
+
console.log(` Slack: ${slackStatus}`);
|
|
676
|
+
} else {
|
|
677
|
+
console.log(' Slack: off');
|
|
678
|
+
}
|
|
679
|
+
}
|
|
637
680
|
console.log('');
|
|
638
681
|
return data.config;
|
|
639
682
|
}
|
|
640
683
|
|
|
641
|
-
//
|
|
684
|
+
// Check for dot-notation heartbeat keys
|
|
685
|
+
const dotKeys = Object.keys(updates).filter(k => k.includes('.'));
|
|
686
|
+
if (dotKeys.length > 0) {
|
|
687
|
+
// Initialize heartbeat config with defaults if not present
|
|
688
|
+
if (!data.config.heartbeat) {
|
|
689
|
+
data.config.heartbeat = getDefaultHeartbeatConfig();
|
|
690
|
+
}
|
|
691
|
+
for (const key of dotKeys) {
|
|
692
|
+
if (key.startsWith('heartbeat.')) {
|
|
693
|
+
const subPath = key.slice('heartbeat.'.length);
|
|
694
|
+
setNestedValue(data.config.heartbeat, subPath, coerceValue(updates[key]));
|
|
695
|
+
} else {
|
|
696
|
+
setNestedValue(data.config, key, coerceValue(updates[key]));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Remove dot-notation keys so they don't fall through to flat handling
|
|
700
|
+
dotKeys.forEach(k => delete updates[k]);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Apply flat updates
|
|
642
704
|
if (updates.maxIterations !== undefined) {
|
|
643
705
|
data.config.maxIterations = parseInt(updates.maxIterations, 10);
|
|
644
706
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coder-config",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.8-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",
|
package/ui/routes/loops.js
CHANGED
|
@@ -598,7 +598,14 @@ function setupLoopHooks(projectPath) {
|
|
|
598
598
|
return installLoopHooks(null, projectPath);
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
+
function getHeartbeat(manager) {
|
|
602
|
+
if (!manager) return { error: 'Manager not available' };
|
|
603
|
+
const { heartbeat: runHeartbeat } = require('../../lib/heartbeat');
|
|
604
|
+
return runHeartbeat(manager.installDir);
|
|
605
|
+
}
|
|
606
|
+
|
|
601
607
|
module.exports = {
|
|
608
|
+
getHeartbeat,
|
|
602
609
|
getLoops,
|
|
603
610
|
getActiveLoop,
|
|
604
611
|
getLoop,
|
package/ui/server.cjs
CHANGED
|
@@ -886,6 +886,10 @@ class ConfigUIServer {
|
|
|
886
886
|
}
|
|
887
887
|
break;
|
|
888
888
|
|
|
889
|
+
case '/api/loops/heartbeat':
|
|
890
|
+
if (req.method === 'GET') return this.json(res, routes.loops.getHeartbeat(this.manager));
|
|
891
|
+
break;
|
|
892
|
+
|
|
889
893
|
case '/api/loops/tune-prompt':
|
|
890
894
|
if (req.method === 'POST') {
|
|
891
895
|
const result = await routes.loops.tunePrompt(
|